We recently had a Growatt MIN 11400TL-XH-US inverter and associated battery system installed as part of our solar power system. It all seems quite reasonable, except that its IoT presence is aggressively tied to the cloud. I’d like it not to be. There’s https://github.com/johanmeijer/grott, for an off-the-shelf approach to this problem, but I was not sufficiently enamored of its implementation to want to run it myself. Nevertheless, it has been a useful resource for cross-checking my understanding of Growatt’s protocols.
Please understand that there may be mistakes in the following and it is still mostly the result of educated guesses from packet dumps. There are no warranties associated with this text, not even implied ones.
Connectivity¶
The inverter’s datalogger – apparently fully integrated, in the case of MIN devices – can be programmed to connect to one of four endpoints, by DNS:
server.growatt.com
server-us.growatt.com
server-cn.growatt.com
server.smten.com
It does so on ports 5279 and 5280. I do not know what transpires on port 5280.
Modified Modbus Header¶
The TCP stream established to port 5279 is reminiscent of “Modbus over TCP/IP”. Every message, in each direction, begins with an 8-byte header and ends with a 2-byte checksum. Multi-byte fields in the header are MSB-first (that is, network-endian).
A 2-byte “transaction identifier” (message sequence number)
A 2-byte “protocol identifier”. While Modbus sets this to 0, Growatt uses their own ontology here.
A 2-byte data length, which counts all bytes after the header, including the aforementioned 2-byte checksum.
A 1-byte unit identifier, should there be multiple devices on the bus.
A 1-byte function code.
The checksum is performed with the standard Modbus CRC-16 polynomial, but is, unlike in Modbus RTU serial streams, here transmitted MSB-first.
Protocols¶
Growatt uses protocol identifiers 6 (and 5, apparently?) to indicate a… let’s say lightly obfuscated payload. Specifically, all post-header non-checksum bytes are xor’d with the mask “Growatt” (in ASCII). The checksum is computed using the obfuscated contents, not their “plaintext”.
Function Codes¶
0x03 and 0x04: Register Dumps¶
The datalogger periodically generates, without in-band stimulus, function code
0x03
and 0x04
messages. The payload of these messages appear to
consist of a 67-byte header followed by an array of register report structures.
The initial header contains…
A 30-byte, NUL-terminated, right NUL-padded ASCII string of the datalogger’s serial number.
A 30-byte, NUL-terminated, right NUL-padded ASCII string of the inverter’s serial number.
A 6-byte date and timestamp. The format for this is
Y M D H M S
with most fields being straightforward.Y
takes the year2000
as its zero value.M
encodes January as1
and counts from there;D
also counts from1
.A 1-byte count of subsequent register report structures.
A register report structure consists of a 4-byte fixed header followed by an array of 2-byte, MSB-first register values. The header contains two 2-byte, MSB-first fields, the minimum (inclusive) and then maximum (inclusive) register indices to follow. See the document “Growatt PV Inverter Modbus RS485 RTU Protocol v120” for the defined list of registers.
Function code 0x03
gives registers in the “holding register” namespace,
while 0x04
gives registers in the “input register” namespace.
The server replies with its own corresponding 0x03
or 0x04
message,
with the same transaction identifier, containing a single byte payload of
0x00
(but obfuscated, of course, as 0x47
or G
).
0x16: Ping¶
Ping messages will be echoed exactly by the server, including an unchanged transaction identifier.
In practice, it looks like the datalogger sends identical payloads for each ping: the same 30-byte string encoding of its serial number as above followed by two zero bytes.
0x18: Server-Initiated Command¶
Function code 0x18
is observed coming from the server.
0x19: Datalogger Identification¶
These short messages consist of the same 30-byte string encoding of the datalogger’s serial number followed by a 4-byte header and a variable length payload. The header is two 2-byte MSB-first fields: a key and the length of the payload string. The known keys are as follows:
Key |
Payload |
4 |
Update interval in minutes, as decimal value (e.g., “5.0”) |
8 |
Datalogger alias (defaults to serial number) |
14 |
Datalogger IP address (dotted quad format) |
16 |
Datalogger MAC address |
18 |
Server TCP port |
19 |
Server DNS name |
21 |
Firmware version string |
The server occasionally replies with its own 0x19
message.