evcc-io/plugin/aa55/aa55udp_test.go

203 lines
6.2 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package aa55
import (
"encoding/hex"
"net"
"testing"
"github.com/evcc-io/evcc/util"
"github.com/evcc-io/evcc/util/modbus"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// Wire-protocol primitives are covered in aa55_test.go.
// This file covers the AA55UDP plugin adapter end-to-end via FloatGetter.
func decodeFor(t *testing.T, name string) (func([]byte) float64, int) {
t.Helper()
reg := modbus.Register{Type: "holding", Decode: name}
length, err := reg.Length()
require.NoError(t, err)
fn, err := reg.DecodeFunc()
require.NoError(t, err)
return fn, int(length) * 2
}
// TestFloatGetter_DT_Power verifies the full query/decode pipeline using the
// GW17K-DT real capture sliced to just the power register bytes: 12470 W.
func TestFloatGetter_DT_Power(t *testing.T) {
response := singleRegResponse(t, capGW17kDT, 54, 4, 0x7f)
dec, n := decodeFor(t, "int32")
p := &AA55UDP{
log: util.NewLogger("test"),
conn: mockConn(t, response),
pdu: buildPDU(0x7F, 0x75AF, 2),
decode: dec,
length: n,
scale: 1.0,
}
getter, err := p.FloatGetter()
require.NoError(t, err)
val, err := getter()
require.NoError(t, err)
assert.InDelta(t, 12470.0, val, 0.5)
}
// TestFloatGetter_DT_Energy verifies scale 0.1: 299844 × 0.1 = 29984.4 kWh.
func TestFloatGetter_DT_Energy(t *testing.T) {
response := singleRegResponse(t, capGW17kDT, 90, 4, 0x7f)
dec, n := decodeFor(t, "uint32")
p := &AA55UDP{
log: util.NewLogger("test"),
conn: mockConn(t, response),
pdu: buildPDU(0x7F, 0x75C1, 2),
decode: dec,
length: n,
scale: 0.1,
}
getter, err := p.FloatGetter()
require.NoError(t, err)
val, err := getter()
require.NoError(t, err)
assert.InDelta(t, 29984.4, val, 0.001)
}
// TestFloatGetter_ET_PV verifies ET pv power: GW10K-ET = 831 W.
func TestFloatGetter_ET_PV(t *testing.T) {
response := singleRegResponse(t, capGW10kET, 74, 4, 0xf7)
dec, n := decodeFor(t, "int32")
p := &AA55UDP{
log: util.NewLogger("test"),
conn: mockConn(t, response),
pdu: buildPDU(0xF7, 0x8941, 2),
decode: dec,
length: n,
scale: 1.0,
}
getter, err := p.FloatGetter()
require.NoError(t, err)
val, err := getter()
require.NoError(t, err)
assert.InDelta(t, 831.0, val, 0.5)
}
// TestFloatGetter_ET_Battery verifies charging battery: GW10K-ET = -2512 W.
func TestFloatGetter_ET_Battery(t *testing.T) {
response := singleRegResponse(t, capGW10kET, 164, 4, 0xf7)
dec, n := decodeFor(t, "int32")
p := &AA55UDP{
log: util.NewLogger("test"),
conn: mockConn(t, response),
pdu: buildPDU(0xF7, 0x896E, 2),
decode: dec,
length: n,
scale: 1.0,
}
getter, err := p.FloatGetter()
require.NoError(t, err)
val, err := getter()
require.NoError(t, err)
assert.InDelta(t, -2512.0, val, 0.5)
}
// TestFloatGetter_ET_SoC verifies SoC: GW10K-ET = 68%.
func TestFloatGetter_ET_SoC(t *testing.T) {
response := singleRegResponse(t, capGW10kETBattery, 14, 2, 0xf7)
dec, n := decodeFor(t, "uint16")
p := &AA55UDP{
log: util.NewLogger("test"),
conn: mockConn(t, response),
pdu: buildPDU(0xF7, 0x908F, 1),
decode: dec,
length: n,
scale: 1.0,
}
getter, err := p.FloatGetter()
require.NoError(t, err)
val, err := getter()
require.NoError(t, err)
assert.InDelta(t, 68.0, val, 0.5)
}
// TestBuildReadConfig_RegisterMode verifies default register-read config.
func TestBuildReadConfig_RegisterMode(t *testing.T) {
cfg, err := buildReadConfig(int(InverterAddr), 0x75AF, 2, nil)
require.NoError(t, err)
assert.Equal(t, []byte{0x7f, 0x03, 0x75, 0xaf, 0x00, 0x02}, cfg.pdu)
assert.Equal(t, 0, cfg.offset)
assert.False(t, cfg.useCache)
}
// TestBuildReadConfig_BlockMode verifies the block PDU is built from the block
// register/count and the target register's offset is computed within it.
// ET grid (0x8943) within block READ 125 @ 0x891C → offset (35139-35100)*2 = 78.
func TestBuildReadConfig_BlockMode(t *testing.T) {
cfg, err := buildReadConfig(0xF7, 0x8943, 2, &modbus.Block{Register: 0x891C, Count: 125})
require.NoError(t, err)
assert.Equal(t, []byte{0xf7, 0x03, 0x89, 0x1c, 0x00, 0x7d}, cfg.pdu)
assert.Equal(t, 78, cfg.offset)
assert.True(t, cfg.useCache)
}
// TestBuildReadConfig_RejectsRegisterOutsideBlock rejects a target register
// that does not fit entirely within the configured block.
func TestBuildReadConfig_RejectsRegisterOutsideBlock(t *testing.T) {
// before block start
_, err := buildReadConfig(0xF7, 0x8900, 2, &modbus.Block{Register: 0x891C, Count: 125})
require.Error(t, err)
// past block end (0x891C+125 = 0x8999; 0x8998+2 overruns)
_, err = buildReadConfig(0xF7, 0x8998, 2, &modbus.Block{Register: 0x891C, Count: 125})
require.Error(t, err)
}
// ---------------------------------------------------------------------------
// Test helpers
// ---------------------------------------------------------------------------
// singleRegResponse builds the AA55 response frame that an inverter would
// return for a single-register read, by slicing the value bytes out of a
// real block-read capture at the given offset.
// src is the inverter source byte (0x7f for DT, 0xf7 for ET).
func singleRegResponse(t *testing.T, capHex string, offset, valueBytes int, src byte) []byte {
t.Helper()
cap, err := hex.DecodeString(capHex)
require.NoError(t, err)
blockPayload, err := stripHeader(cap)
require.NoError(t, err)
require.GreaterOrEqual(t, len(blockPayload), offset+valueBytes)
value := blockPayload[offset : offset+valueBytes]
frame := []byte{0xAA, 0x55, src, 0x03, byte(valueBytes)}
frame = append(frame, value...)
frame = append(frame, 0x00, 0x00) // CRC not validated by stripHeader
return frame
}
// mockConn starts a UDP server that responds with response to every packet,
// and returns a *net.UDPConn already dialled at that server.
func mockConn(t *testing.T, response []byte) *net.UDPConn {
t.Helper()
srv, err := net.ListenPacket("udp4", "127.0.0.1:0")
require.NoError(t, err)
t.Cleanup(func() { srv.Close() })
go func() {
buf := make([]byte, 512)
for {
_, addr, err := srv.ReadFrom(buf)
if err != nil {
return
}
_, _ = srv.WriteTo(response, addr)
}
}()
addr, err := net.ResolveUDPAddr("udp4", srv.LocalAddr().String())
require.NoError(t, err)
conn, err := net.DialUDP("udp4", nil, addr)
require.NoError(t, err)
t.Cleanup(func() { conn.Close() })
return conn
}