Provisional support for TL-XH2 (and maybe TL-XH ?) inverters - #185
Merged
Merged
Conversation
The ShineWiFi-X2 dongle used by Growatt's TL-XH hybrid inverter family produces two issues with the current GroBro: 1. The SUBSCRIBE topic from the dongle contains stray trailing bytes (observed: `s/33/ZGQ0F5601J?\x18`). The existing isprintable()-only filter strips the 0x18 control character but keeps the `?`, producing a device_id like `ZGQ0F5601J?` that doesn't match any prefix. 2. The ZGQ serial prefix isn't recognized as a known device family, leading to "Modbus message from unknown device type" log spam and no HA entities being created. Changes: - Replace the `isprintable()` device_id extractor with an alphanumeric-only regex. Growatt serials are exclusively A-Z 0-9, so this is a safe tightening for all device families (still passes the existing test suite untouched). Centralize the logic in `_extract_device_id()`. - Add a `KNOWN_XH_REGISTERS` set loaded from `grobro/model/growatt_xh_registers.json`. Provisional: the JSON is currently a copy of the NEO register map as a starting point. Actual XH/TL-XH/TL-XH2 register definitions need to be verified against live data — likely closer to SPH (single-phase hybrid) semantics from Grott's legacy protocol definitions. - Route the ZGQ prefix to KNOWN_XH_REGISTERS in both `grobro/grobro/client.py` (modbus + config message routing) and `grobro/ha/client.py` (`get_known_registers`, `get_device_type_name` returns "MIN-XH"). - Add tests for `_extract_device_id` covering the QMN/0PVP/ZGQ cases and the trailing-control-char + `?` quirks. Tested with: MIN 4200TL-XH2 inverter, ShineWiFi-X2 dongle, firmware sw_version=7.6.1.8. Dongle connects via TLS (port 7006 on EMQX), SUBSCRIBE accepted, NOAH-style config message recognized (`Received NOAH config for ZGQ0F5601J sw_version=7.6.1.8`), and the device is now matched by ZGQ-prefix routing. The exact register layout for XH2 modbus payloads still needs validation; this commit unblocks the pipeline so users can iterate on register definitions. Refs: discussion in johanmeijer/grott#592 (XH dongle protocol change) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The NEO-as-XH register copy already contained 106 fields with HA discovery
metadata that were just gated behind publish:false. Verified against live
data from a Growatt MIN 4200TL-XH2 that these registers decode correctly:
Grid power (live, W):
Ptouser_total → "Grid import power"
Ptogrid_total → "Grid export power"
Ptoload_total → "Load power"
Grid energy (cumulative, kWh):
Etouser_today / Etouser_total → "Grid import today / Lifetime"
Etogrid_today / Etogrid_total → "Grid export today / Lifetime"
Eload_today / Eload_total → "Load consumption today / Lifetime"
These are the fields needed for HA's Energy Dashboard.
Also enabled battery + BMS fields. They will read 0 until a battery is
physically installed (no-battery configuration confirmed at test setup).
Register positions inherited from NEO; verification against a real XH
battery installation is still pending — values may need tweaking but the
HA entities will at least exist:
Battery state:
Vbat, Ibat, SOC, Pchr, Pdischr
Echr_today/total, Edischr_today/total, Eacchr_today/total
BMS:
BMS_Status, BMS_SOC, BMS_BatteryVolt, BMS_BatteryCurr,
BMS_BatteryTemp, BMS_SOH, BMS_CycleCnt
BDC: BDC_OnOffState
Diagnostics:
Inverter_Status, Fault_code, Warning_code, N_Bus_Voltage, Time_total
Friendly names rewritten where the original was ambiguous
("Total forward power" → "Grid import power", etc.).
Test suite passes unchanged (342 tests).
The enabling script (scripts/enable_xh_entities.py) is checked in so the
mapping is reviewable and reusable when more fields are validated.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous naming implied this code supports the entire MIN TL-XH family, but: - XH gen 1 uses the legacy port-5279 protocol handled by Grott, not GroBro - XH gen 2 uses the new encrypted MQTT-on-7006 protocol (this fork) - The ZGQ serial prefix and the ShineWiFi-X2 dongle are gen-2 specific We only verified against a MIN 4200TL-XH2 inverter, so the constants/files should reflect that scope. KNOWN_XH_REGISTERS → KNOWN_XH2_REGISTERS growatt_xh_registers.json → growatt_xh2_registers.json enable_xh_entities.py → enable_xh2_entities.py get_device_type_name "MIN-XH" → "MIN-XH2" No functional changes; tests still pass (342). If someone later wants to add XH gen 1 support, that would be a separate register set (likely closer to Grott's SPH layout) and prefix, not a rename of this one. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Owner
|
Thank you very much for this very valuable contribution! 😊 |
robertzaage
marked this pull request as ready for review
May 19, 2026 21:44
Welcome to Codecov 🎉Once you merge this PR into your default branch, you're all set! Codecov will compare coverage reports and display results in all future pull requests. ℹ️ You can also turn on project coverage checks and project coverage reporting on Pull Request comment Thanks for integrating Codecov - We've got you covered ☂️ |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Notice: This was built with heavy claude code assistance (for development, for parsing tcpdump pcap logs, and for debugging mqtt connections).
I recently got a Growatt inverter installed. originally my quick research told me it would be a somewhat friendly brand to homeassistant. oh boy, it wasn't. seems my dongle had some issues that other models didn't suffer from, but I finally got a working setup
Inverter model: 4200TL-XH2
dongle: ShineWifi-X2 (connected via wifi)
TL;DR of what I faced:
s/33/ZGQ0F5601J?\x18). This required a small patch in grobro, and also caused mosquitto (both 3.x and 2.x to fail and refuse that connection due to malformed data). Luckily exmq accepted itOriginal PR summary from claude
Details
Changes:
Replace the
isprintable()device_id extractor with an alphanumeric-onlyregex. Growatt serials are exclusively A-Z 0-9, so this is a safe
tightening for all device families (still passes the existing test
suite untouched). Centralize the logic in
_extract_device_id().Add a
KNOWN_XH_REGISTERSset loaded fromgrobro/model/growatt_xh_registers.json. Provisional: the JSON iscurrently a copy of the NEO register map as a starting point.
Actual XH/TL-XH/TL-XH2 register definitions need to be verified
against live data — likely closer to SPH (single-phase hybrid)
semantics from Grott's legacy protocol definitions.
Route the ZGQ prefix to KNOWN_XH_REGISTERS in both
grobro/grobro/client.py(modbus + config message routing) andgrobro/ha/client.py(get_known_registers,get_device_type_namereturns "MIN-XH").
Add tests for
_extract_device_idcovering the QMN/0PVP/ZGQ casesand the trailing-control-char +
?quirks.Tested with: MIN 4200TL-XH2 inverter, ShineWiFi-X2 dongle,
firmware sw_version=7.6.1.8. Dongle connects via TLS (port 7006 on EMQX),
SUBSCRIBE accepted, NOAH-style config message recognized
(
Received NOAH config for ZGQ0F5601J sw_version=7.6.1.8), and thedevice is now matched by ZGQ-prefix routing. The exact register layout
for XH2 modbus payloads still needs validation; this commit unblocks
the pipeline so users can iterate on register definitions.
Refs: discussion in johanmeijer/grott#592 (XH dongle protocol change)
Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com
My docker current compose
Details
the target mqtt (10.6.10.12) is hosted separately from this
HA screenshot (partial, too many entities to show)