55aa: fix PDU slave id for ET series (#28828)
This commit is contained in:
parent
536ebcb319
commit
397b3c003e
3 changed files with 47 additions and 24 deletions
|
|
@ -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),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue