From 397b3c003eb5f4feeef1ee113ce5ae24f8450549 Mon Sep 17 00:00:00 2001 From: Klaas Gadeyne Date: Mon, 6 Apr 2026 09:52:29 +0200 Subject: [PATCH] 55aa: fix PDU slave id for ET series (#28828) --- plugin/aa55udp.go | 27 +++++++++------- plugin/aa55udp_test.go | 31 ++++++++++++------- .../definition/meter/goodwe-wifi-et.yaml | 13 +++++++- 3 files changed, 47 insertions(+), 24 deletions(-) diff --git a/plugin/aa55udp.go b/plugin/aa55udp.go index 84c95258d..c8f384a51 100644 --- a/plugin/aa55udp.go +++ b/plugin/aa55udp.go @@ -33,7 +33,7 @@ type AA55UDP struct { log *util.Logger conn *net.UDPConn pdu []byte // 6-byte PDU body, no CRC - decode string // int32be | uint32be | int16be | uint16be + decode string // int32be | uint32be | int16be | uint16be | float32be scale float64 } @@ -45,18 +45,21 @@ func init() { // // source: aa55udp // host: 192.168.1.26 # inverter IP; port 8899 is always used +// id: 0x7F # inverter address byte: 0x7F for DT/DNS/ES/EM, 0xF7 for ET/EH/BT/BH // register: 30127 # Modbus register address (0-based, uint16) // count: 2 # number of registers to read (1=U16, 2=S32/U32) -// decode: int32be # int32be | uint32be | int16be | uint16be +// decode: int32be # int32be | uint32be | int16be | uint16be | float32be // scale: 1.0 # optional multiplier (default 1.0) func NewAA55UDPFromConfig(_ context.Context, other map[string]interface{}) (Plugin, error) { cc := struct { Host string `mapstructure:"host"` + Id int `mapstructure:"id"` Register uint16 `mapstructure:"register"` Count uint16 `mapstructure:"count"` Decode string `mapstructure:"decode"` Scale float64 `mapstructure:"scale"` }{ + Id: int(aa55InverterAddr), Count: 2, Scale: 1.0, } @@ -68,13 +71,17 @@ func NewAA55UDPFromConfig(_ context.Context, other map[string]interface{}) (Plug return nil, errors.New("aa55udp: count must be ≥ 1") } + if cc.Id < 0 || cc.Id > 255 { + return nil, fmt.Errorf("aa55udp: id must be 0-255, got %d", cc.Id) + } + switch cc.Decode { case "int32be", "uint32be", "int16be", "uint16be", "float32be": default: return nil, fmt.Errorf("aa55udp: unsupported decode %q (want int32be|uint32be|int16be|uint16be|float32be)", cc.Decode) } - pdu := buildPDU(cc.Register, cc.Count) + pdu := buildPDU(byte(cc.Id), cc.Register, cc.Count) addr, err := net.ResolveUDPAddr("udp4", net.JoinHostPort(cc.Host, "8899")) if err != nil { @@ -132,20 +139,18 @@ func (p *AA55UDP) query() (float64, error) { return v * p.scale, nil } -// aa55InverterAddr is the target address byte used in all AA55 request PDUs. -// The GoodWe WiFi module always uses 0x7F as the inverter target address, -// regardless of inverter family. The family only affects the source byte in -// responses (0x7F for DT/DNS, 0xF7 for ET/EH/BT/BH). +// aa55InverterAddr is the default inverter address byte, used by DT/DNS and ES/EM families. +// ET/EH/BT/BH families require 0xF7 instead. const aa55InverterAddr = 0x7F // aa55ReadFunc is the Modbus function code for READ HOLDING REGISTERS. const aa55ReadFunc = 0x03 -// buildPDU constructs the 6-byte Modbus PDU body for a READ HOLDING REGISTERS -// request targeting the GoodWe WiFi module. -func buildPDU(register, count uint16) []byte { +// buildPDU constructs the 6-byte PDU for a READ HOLDING REGISTERS request. +// addr is the inverter address byte: 0x7F for DT/DNS/ES/EM, 0xF7 for ET/EH/BT/BH. +func buildPDU(addr byte, register, count uint16) []byte { return []byte{ - aa55InverterAddr, aa55ReadFunc, + addr, aa55ReadFunc, byte(register >> 8), byte(register), byte(count >> 8), byte(count), } diff --git a/plugin/aa55udp_test.go b/plugin/aa55udp_test.go index 22e8759e4..0ebefb259 100644 --- a/plugin/aa55udp_test.go +++ b/plugin/aa55udp_test.go @@ -63,20 +63,27 @@ const ( func TestBuildPDU_DTpower(t *testing.T) { // DT power: register 0x75AF, count 2 → READ 2 @ 0x75AF - got := buildPDU(0x75AF, 2) + got := buildPDU(0x7F, 0x75AF, 2) assert.Equal(t, []byte{0x7f, 0x03, 0x75, 0xaf, 0x00, 0x02}, got) } +func TestBuildPDU_DefaultAddress(t *testing.T) { + // When address is omitted from config, aa55InverterAddr (0x7F) must be used. + // This guards existing DT/DNS and ES/EM setups that rely on the default. + got := buildPDU(aa55InverterAddr, 0x75AF, 2) + assert.Equal(t, byte(0x7F), got[0], "default address byte must be 0x7F") +} + func TestBuildPDU_ETgrid(t *testing.T) { // ET grid: register 0x8943, count 2 - got := buildPDU(0x8943, 2) - assert.Equal(t, []byte{0x7f, 0x03, 0x89, 0x43, 0x00, 0x02}, got) + got := buildPDU(0xF7, 0x8943, 2) + assert.Equal(t, []byte{0xf7, 0x03, 0x89, 0x43, 0x00, 0x02}, got) } func TestBuildPDU_SoC(t *testing.T) { // ET SoC: register 0x908F, count 1 (U16) - got := buildPDU(0x908F, 1) - assert.Equal(t, []byte{0x7f, 0x03, 0x90, 0x8f, 0x00, 0x01}, got) + got := buildPDU(0xF7, 0x908F, 1) + assert.Equal(t, []byte{0xf7, 0x03, 0x90, 0x8f, 0x00, 0x01}, got) } // --------------------------------------------------------------------------- @@ -179,7 +186,7 @@ func TestDecodeAt_UnknownType(t *testing.T) { func TestModbusCRC16_DTPdu(t *testing.T) { // DT power PDU 7f 03 75 af 00 02 → CRC d1 ba - pdu := buildPDU(0x75AF, 2) + pdu := buildPDU(0x7F, 0x75AF, 2) crc := modbusCRC16(pdu) // Verify round-trip: CRC is 2 bytes and deterministic assert.Len(t, crc, 2) @@ -188,7 +195,7 @@ func TestModbusCRC16_DTPdu(t *testing.T) { func TestModbusCRC16_ETPdu(t *testing.T) { // ET grid PDU 7f 03 89 43 00 02 → CRC is 2 bytes - pdu := buildPDU(0x8943, 2) + pdu := buildPDU(0xF7, 0x8943, 2) crc := modbusCRC16(pdu) assert.Len(t, crc, 2) assert.Equal(t, crc, modbusCRC16(pdu)) @@ -292,7 +299,7 @@ func TestFloatGetter_DT_Power(t *testing.T) { p := &AA55UDP{ log: util.NewLogger("test"), conn: mockConn(t, response), - pdu: buildPDU(0x75AF, 2), + pdu: buildPDU(0x7F, 0x75AF, 2), decode: "int32be", scale: 1.0, } @@ -309,7 +316,7 @@ func TestFloatGetter_DT_Energy(t *testing.T) { p := &AA55UDP{ log: util.NewLogger("test"), conn: mockConn(t, response), - pdu: buildPDU(0x75C1, 2), + pdu: buildPDU(0x7F, 0x75C1, 2), decode: "uint32be", scale: 0.1, } @@ -326,7 +333,7 @@ func TestFloatGetter_ET_PV(t *testing.T) { p := &AA55UDP{ log: util.NewLogger("test"), conn: mockConn(t, response), - pdu: buildPDU(0x8941, 2), + pdu: buildPDU(0xF7, 0x8941, 2), decode: "int32be", scale: 1.0, } @@ -343,7 +350,7 @@ func TestFloatGetter_ET_Battery(t *testing.T) { p := &AA55UDP{ log: util.NewLogger("test"), conn: mockConn(t, response), - pdu: buildPDU(0x896E, 2), + pdu: buildPDU(0xF7, 0x896E, 2), decode: "int32be", scale: 1.0, } @@ -360,7 +367,7 @@ func TestFloatGetter_ET_SoC(t *testing.T) { p := &AA55UDP{ log: util.NewLogger("test"), conn: mockConn(t, response), - pdu: buildPDU(0x908F, 1), + pdu: buildPDU(0xF7, 0x908F, 1), decode: "uint16be", scale: 1.0, } diff --git a/templates/definition/meter/goodwe-wifi-et.yaml b/templates/definition/meter/goodwe-wifi-et.yaml index f2c8bb44f..0fdb7121f 100644 --- a/templates/definition/meter/goodwe-wifi-et.yaml +++ b/templates/definition/meter/goodwe-wifi-et.yaml @@ -4,23 +4,29 @@ requirements: products: - brand: GoodWe description: - generic: EH/BT/BH Hybrid Inverter (WiFi) + generic: ET/EH/BT/BH Hybrid Inverter (WiFi) + params: - name: usage choice: ["grid", "pv", "battery"] + - name: host + example: 192.168.1.26 + render: | type: custom {{- if eq .usage "grid" }} power: source: aa55udp host: {{ .host }} + id: 247 register: 35139 count: 2 decode: int32be energy: source: aa55udp host: {{ .host }} + id: 247 register: 36017 count: 2 decode: float32be @@ -30,12 +36,14 @@ render: | power: source: aa55udp host: {{ .host }} + id: 247 register: 35137 count: 2 decode: int32be energy: source: aa55udp host: {{ .host }} + id: 247 register: 35191 count: 2 decode: uint32be @@ -45,12 +53,14 @@ render: | power: source: aa55udp host: {{ .host }} + id: 247 register: 35182 count: 2 decode: int32be energy: source: aa55udp host: {{ .host }} + id: 247 register: 35209 count: 2 decode: uint32be @@ -58,6 +68,7 @@ render: | soc: source: aa55udp host: {{ .host }} + id: 247 register: 37007 count: 1 decode: uint16be