diff --git a/plugin/aa55/aa55.go b/plugin/aa55/aa55.go index 70b68e5f8..63c713427 100644 --- a/plugin/aa55/aa55.go +++ b/plugin/aa55/aa55.go @@ -8,10 +8,7 @@ package aa55 import ( - "encoding/binary" "errors" - "fmt" - "math" "github.com/grid-x/modbus" ) @@ -60,80 +57,3 @@ func modbusCRC16(data []byte) []byte { } return []byte{byte(crc & 0xFF), byte(crc >> 8)} } - -// decodeMeta describes the properties of a supported decode type. -type decodeMeta struct { - size int -} - -func decodeMetadata(name string) (decodeMeta, bool) { - switch name { - case "float32be", "int32be", "uint32be", "uint32nan": - return decodeMeta{size: 4}, true - case "int16be", "uint16be": - return decodeMeta{size: 2}, true - default: - return decodeMeta{}, false - } -} - -// validateDecode returns an error if decode is not a supported type. -func validateDecode(decode string) error { - if _, ok := decodeMetadata(decode); !ok { - return fmt.Errorf("unsupported decode %q (want int32be|uint32be|uint32nan|int16be|uint16be|float32be)", decode) - } - return nil -} - -// decodeSize returns the number of bytes required to decode the given type. -// Panics if decode type is unknown — callers must validateDecode first. -func decodeSize(decode string) int { - if info, ok := decodeMetadata(decode); ok { - return info.size - } - panic(fmt.Sprintf("unknown decode type %q", decode)) -} - -// decodeAt extracts a value at the given byte offset of payload and interprets -// it according to decode. -func decodeAt(payload []byte, offset int, decode string) (float64, error) { - switch decode { - case "float32be": - if len(payload) < offset+4 { - return 0, fmt.Errorf("payload too short for float32be at offset %d (len=%d)", offset, len(payload)) - } - bits := binary.BigEndian.Uint32(payload[offset:]) - return float64(math.Float32frombits(bits)), nil - case "int32be": - if len(payload) < offset+4 { - return 0, fmt.Errorf("payload too short for int32be at offset %d (len=%d)", offset, len(payload)) - } - return float64(int32(binary.BigEndian.Uint32(payload[offset:]))), nil - case "uint32be": - if len(payload) < offset+4 { - return 0, fmt.Errorf("payload too short for uint32be at offset %d (len=%d)", offset, len(payload)) - } - return float64(binary.BigEndian.Uint32(payload[offset:])), nil - case "uint32nan": - // Like uint32be but treats 0xFFFFFFFF (not-connected sentinel) as 0. - // Used for PV string power registers where disconnected strings report NaN. - if len(payload) < offset+4 { - return 0, fmt.Errorf("payload too short for uint32nan at offset %d (len=%d)", offset, len(payload)) - } - if v := binary.BigEndian.Uint32(payload[offset:]); v != 0xFFFFFFFF { - return float64(v), nil - } - return 0, nil - case "int16be": - if len(payload) < offset+2 { - return 0, fmt.Errorf("payload too short for int16be at offset %d (len=%d)", offset, len(payload)) - } - return float64(int16(binary.BigEndian.Uint16(payload[offset:]))), nil - case "uint16be": - if len(payload) < offset+2 { - return 0, fmt.Errorf("payload too short for uint16be at offset %d (len=%d)", offset, len(payload)) - } - return float64(binary.BigEndian.Uint16(payload[offset:])), nil - } - return 0, fmt.Errorf("unknown decode type: %s", decode) -} diff --git a/plugin/aa55/aa55_test.go b/plugin/aa55/aa55_test.go index 72826c67d..b8acefbda 100644 --- a/plugin/aa55/aa55_test.go +++ b/plugin/aa55/aa55_test.go @@ -1,11 +1,10 @@ package aa55 import ( - "encoding/binary" "encoding/hex" - "math" "testing" + "github.com/evcc-io/evcc/util/modbus" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -108,107 +107,6 @@ func TestStripHeader_Short(t *testing.T) { assert.Contains(t, err.Error(), "short") } -// --------------------------------------------------------------------------- -// decodeAt -// --------------------------------------------------------------------------- - -func TestDecodeAt_Int32BE_Positive(t *testing.T) { - payload := make([]byte, 4) - binary.BigEndian.PutUint32(payload, uint32(int32(1972))) - v, err := decodeAt(payload, 0, "int32be") - require.NoError(t, err) - assert.InDelta(t, 1972.0, v, 0) -} - -func TestDecodeAt_Int32BE_Negative(t *testing.T) { - payload := make([]byte, 4) - v32 := int32(-2512) - binary.BigEndian.PutUint32(payload, uint32(v32)) - v, err := decodeAt(payload, 0, "int32be") - require.NoError(t, err) - assert.InDelta(t, -2512.0, v, 0) -} - -func TestDecodeAt_Uint32BE(t *testing.T) { - payload := make([]byte, 4) - binary.BigEndian.PutUint32(payload, 43513) - v, err := decodeAt(payload, 0, "uint32be") - require.NoError(t, err) - assert.InDelta(t, 43513.0, v, 0) -} - -func TestDecodeAt_Uint16BE(t *testing.T) { - payload := make([]byte, 2) - binary.BigEndian.PutUint16(payload, 68) - v, err := decodeAt(payload, 0, "uint16be") - require.NoError(t, err) - assert.InDelta(t, 68.0, v, 0) -} - -func TestDecodeAt_Int16BE_Negative(t *testing.T) { - payload := make([]byte, 2) - v16 := int16(-300) - binary.BigEndian.PutUint16(payload, uint16(v16)) - v, err := decodeAt(payload, 0, "int16be") - require.NoError(t, err) - assert.InDelta(t, -300.0, v, 0) -} - -func TestDecodeAt_Float32BE(t *testing.T) { - payload := make([]byte, 4) - binary.BigEndian.PutUint32(payload, math.Float32bits(123.456)) - v, err := decodeAt(payload, 0, "float32be") - require.NoError(t, err) - assert.InDelta(t, 123.456, v, 0.001) -} - -func TestDecodeAt_Uint32NAN_Normal(t *testing.T) { - payload := make([]byte, 4) - binary.BigEndian.PutUint32(payload, 8310) // e.g. 83.10 W string power - v, err := decodeAt(payload, 0, "uint32nan") - require.NoError(t, err) - assert.InDelta(t, 8310.0, v, 0) -} - -func TestDecodeAt_Uint32NAN_Disconnected(t *testing.T) { - payload := make([]byte, 4) - binary.BigEndian.PutUint32(payload, 0xFFFFFFFF) // disconnected string sentinel - v, err := decodeAt(payload, 0, "uint32nan") - require.NoError(t, err) - assert.InDelta(t, 0.0, v, 0) // must return 0, not 4.3GW -} - -func TestDecodeAt_TooShort(t *testing.T) { - _, err := decodeAt([]byte{0x00}, 0, "int32be") - require.Error(t, err) -} - -func TestDecodeAt_UnknownType(t *testing.T) { - _, err := decodeAt(make([]byte, 4), 0, "float32") - require.Error(t, err) -} - -// --------------------------------------------------------------------------- -// validateDecode / decodeSize -// --------------------------------------------------------------------------- - -func TestValidateDecode_OK(t *testing.T) { - for _, d := range []string{"int32be", "uint32be", "uint32nan", "int16be", "uint16be", "float32be"} { - assert.NoError(t, validateDecode(d), d) - } -} - -func TestValidateDecode_Reject(t *testing.T) { - assert.Error(t, validateDecode("float32")) - assert.Error(t, validateDecode("")) -} - -func TestDecodeSize(t *testing.T) { - assert.Equal(t, 4, decodeSize("int32be")) - assert.Equal(t, 4, decodeSize("uint32nan")) - assert.Equal(t, 2, decodeSize("uint16be")) -} - // --------------------------------------------------------------------------- // modbusCRC16 // --------------------------------------------------------------------------- @@ -243,63 +141,63 @@ func TestModbusCRC16_KnownValue(t *testing.T) { // --------------------------------------------------------------------------- func TestDT_Power_GW3000DNS30(t *testing.T) { - assertBlockOffset(t, capGW3000DNS30, 54, "int32be", 1.0, 1972.0) + assertBlockOffset(t, capGW3000DNS30, 54, "int32", 1.0, 1972.0) } func TestDT_Power_GW17K(t *testing.T) { - assertBlockOffset(t, capGW17kDT, 54, "int32be", 1.0, 12470.0) + assertBlockOffset(t, capGW17kDT, 54, "int32", 1.0, 12470.0) } func TestDT_Power_GW20KAU(t *testing.T) { - assertBlockOffset(t, capGW20kAUDT, 54, "int32be", 1.0, 4957.0) + assertBlockOffset(t, capGW20kAUDT, 54, "int32", 1.0, 4957.0) } func TestDT_Energy_GW17K(t *testing.T) { - assertBlockOffset(t, capGW17kDT, 90, "uint32be", 0.1, 29984.4) + assertBlockOffset(t, capGW17kDT, 90, "uint32", 0.1, 29984.4) } func TestDT_Energy_GW6000(t *testing.T) { - assertBlockOffset(t, capGW6000DT, 90, "uint32be", 0.1, 13350.2) + assertBlockOffset(t, capGW6000DT, 90, "uint32", 0.1, 13350.2) } func TestDT_Energy_GW20KAU(t *testing.T) { - assertBlockOffset(t, capGW20kAUDT, 90, "uint32be", 0.1, 4304.8) + assertBlockOffset(t, capGW20kAUDT, 90, "uint32", 0.1, 4304.8) } func TestET_PV_GW10K(t *testing.T) { - assertBlockOffset(t, capGW10kET, 74, "int32be", 1.0, 831.0) + assertBlockOffset(t, capGW10kET, 74, "int32", 1.0, 831.0) } func TestET_Grid_GW10K_TinyExport(t *testing.T) { - assertBlockOffset(t, capGW10kET, 78, "int32be", 1.0, -3.0) + assertBlockOffset(t, capGW10kET, 78, "int32", 1.0, -3.0) } func TestET_Grid_GW25K_Importing(t *testing.T) { - assertBlockOffset(t, capGW25kET, 78, "int32be", 1.0, 1511.0) + assertBlockOffset(t, capGW25kET, 78, "int32", 1.0, 1511.0) } func TestET_Grid_GW29K9_Exporting(t *testing.T) { - assertBlockOffset(t, capGW29k9ET, 78, "int32be", 1.0, -5403.0) + assertBlockOffset(t, capGW29k9ET, 78, "int32", 1.0, -5403.0) } func TestET_Battery_GW10K_Charging(t *testing.T) { - assertBlockOffset(t, capGW10kET, 164, "int32be", 1.0, -2512.0) + assertBlockOffset(t, capGW10kET, 164, "int32", 1.0, -2512.0) } func TestET_Energy_GW10K(t *testing.T) { - assertBlockOffset(t, capGW10kET, 182, "uint32be", 0.1, 6085.3) + assertBlockOffset(t, capGW10kET, 182, "uint32", 0.1, 6085.3) } func TestET_Energy_GW25K(t *testing.T) { - assertBlockOffset(t, capGW25kET, 182, "uint32be", 0.1, 160.3) + assertBlockOffset(t, capGW25kET, 182, "uint32", 0.1, 160.3) } func TestET_SoC_GW10K(t *testing.T) { - assertBlockOffset(t, capGW10kETBattery, 14, "uint16be", 1.0, 68.0) + assertBlockOffset(t, capGW10kETBattery, 14, "uint16", 1.0, 68.0) } func TestET_SoC_GW25K(t *testing.T) { - assertBlockOffset(t, capGW25kETBattery, 14, "uint16be", 1.0, 100.0) + assertBlockOffset(t, capGW25kETBattery, 14, "uint16", 1.0, 100.0) } // --------------------------------------------------------------------------- @@ -317,7 +215,11 @@ func assertBlockOffset(t *testing.T, capHex string, offset int, decode string, s t.Helper() payload, err := stripHeader(mustHex(t, capHex)) require.NoError(t, err) - v, err := decodeAt(payload, offset, decode) + reg := modbus.Register{Type: "holding", Decode: decode} + length, err := reg.Length() require.NoError(t, err) + fn, err := reg.DecodeFunc() + require.NoError(t, err) + v := fn(payload[offset : offset+int(length)*2]) assert.InDelta(t, expected, v*scale, 0.05) } diff --git a/plugin/aa55/aa55udp.go b/plugin/aa55/aa55udp.go index a0dbfca36..0aa601efd 100644 --- a/plugin/aa55/aa55udp.go +++ b/plugin/aa55/aa55udp.go @@ -31,9 +31,10 @@ var cache = modbus.NewCache(cacheTTL) type AA55UDP struct { log *util.Logger conn *net.UDPConn - pdu []byte // 6-byte PDU body, no CRC - offset int // byte offset into the response payload (0 for register reads) - decode string // int32be | uint32be | uint32nan | int16be | uint16be | float32be + pdu []byte // 6-byte PDU body, no CRC + offset int // byte offset into the response payload (0 for register reads) + length int // value length in bytes + decode func([]byte) float64 // modbus register decoder scale float64 cacheKey string // precomputed cache key (remoteAddr/pdu); empty disables caching delay time.Duration // minimum gap between sends to the inverter (0 disables) @@ -77,14 +78,18 @@ func buildReadConfig(id int, register, count uint16, block *modbus.Block) (readC return readConfig{pdu: buildPDU(byte(id), register, count), useCache: false}, nil } -// New constructs an AA55UDP from a high-level configuration. It validates -// decode, resolves the read mode (register vs block), and wraps the conn. -// The caller is responsible for dialling conn. -func New(log *util.Logger, conn *net.UDPConn, id int, register, count uint16, block *modbus.Block, decode string, scale float64, delay time.Duration) (*AA55UDP, error) { - if err := validateDecode(decode); err != nil { +// New constructs an AA55UDP from a high-level configuration. The register count +// and decoder are derived from reg; the read mode is resolved from block. +func New(log *util.Logger, conn *net.UDPConn, id int, reg modbus.Register, block *modbus.Block, scale float64, delay time.Duration) (*AA55UDP, error) { + count, err := reg.Length() + if err != nil { return nil, err } - cfg, err := buildReadConfig(id, register, count, block) + decode, err := reg.DecodeFunc() + if err != nil { + return nil, err + } + cfg, err := buildReadConfig(id, reg.Address, count, block) if err != nil { return nil, err } @@ -92,6 +97,7 @@ func New(log *util.Logger, conn *net.UDPConn, id int, register, count uint16, bl log: log, conn: conn, decode: decode, + length: int(count) * 2, scale: scale, delay: delay, pdu: cfg.pdu, @@ -115,17 +121,12 @@ func (p *AA55UDP) query() (float64, error) { return 0, err } - minLen := p.offset + decodeSize(p.decode) - if len(payload) < minLen { - return 0, fmt.Errorf("payload too short (len=%d, need=%d)", len(payload), minLen) + end := p.offset + p.length + if len(payload) < end { + return 0, fmt.Errorf("payload too short (len=%d, need=%d)", len(payload), end) } - v, err := decodeAt(payload, p.offset, p.decode) - if err != nil { - return 0, err - } - - return v * p.scale, nil + return p.decode(payload[p.offset:end]) * p.scale, nil } // fetch returns the response payload. In block-read mode the shared cache diff --git a/plugin/aa55/aa55udp_test.go b/plugin/aa55/aa55udp_test.go index 58b1cd098..d8636cb94 100644 --- a/plugin/aa55/aa55udp_test.go +++ b/plugin/aa55/aa55udp_test.go @@ -11,19 +11,30 @@ import ( "github.com/stretchr/testify/require" ) -// Wire-protocol primitives (buildPDU, stripHeader, DecodeAt, ModbusCRC16, -// Cache, real-capture register tests) are covered in aa55_test.go. +// 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: "int32be", + decode: dec, + length: n, scale: 1.0, } getter, err := p.FloatGetter() @@ -36,11 +47,13 @@ func TestFloatGetter_DT_Power(t *testing.T) { // 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: "uint32be", + decode: dec, + length: n, scale: 0.1, } getter, err := p.FloatGetter() @@ -53,11 +66,13 @@ func TestFloatGetter_DT_Energy(t *testing.T) { // 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: "int32be", + decode: dec, + length: n, scale: 1.0, } getter, err := p.FloatGetter() @@ -70,11 +85,13 @@ func TestFloatGetter_ET_PV(t *testing.T) { // 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: "int32be", + decode: dec, + length: n, scale: 1.0, } getter, err := p.FloatGetter() @@ -87,11 +104,13 @@ func TestFloatGetter_ET_Battery(t *testing.T) { // 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: "uint16be", + decode: dec, + length: n, scale: 1.0, } getter, err := p.FloatGetter() diff --git a/plugin/aa55udp.go b/plugin/aa55udp.go index 7b2320ce7..3aa2183f7 100644 --- a/plugin/aa55udp.go +++ b/plugin/aa55udp.go @@ -60,11 +60,6 @@ func NewAA55UDPFromConfig(ctx context.Context, other map[string]any) (Plugin, er return nil, err } - count, err := cc.Register.Length() - if err != nil { - return nil, err - } - raddr, err := net.ResolveUDPAddr("udp4", net.JoinHostPort(cc.Host, "8899")) if err != nil { return nil, err @@ -76,7 +71,7 @@ func NewAA55UDPFromConfig(ctx context.Context, other map[string]any) (Plugin, er return nil, err } - res, err := aa55.New(util.NewLogger("aa55udp"), conn, cc.Id, cc.Register.Address, count, cc.Block, cc.Register.Decode, cc.Scale, cc.Delay) + res, err := aa55.New(util.NewLogger("aa55udp"), conn, cc.Id, cc.Register, cc.Block, cc.Scale, cc.Delay) if err != nil { return nil, fmt.Errorf("aa55udp: %w", err) } diff --git a/plugin/aa55udp_test.go b/plugin/aa55udp_test.go index 3f5cc8a5e..08178e4e1 100644 --- a/plugin/aa55udp_test.go +++ b/plugin/aa55udp_test.go @@ -8,12 +8,12 @@ import ( ) // TestAA55UDPFromConfig_Block verifies the modbus.Register and nested block map -// decode, with the count (2 registers) derived from the int32be decode width. +// decode, with the count (2 registers) derived from the int32 decode width. func TestAA55UDPFromConfig_Block(t *testing.T) { _, err := NewAA55UDPFromConfig(context.Background(), map[string]any{ "host": "127.0.0.1", "id": 247, - "register": map[string]any{"address": 35139, "decode": "int32be"}, + "register": map[string]any{"address": 35139, "decode": "int32"}, "block": map[string]any{"register": 35100, "count": 125}, }) require.NoError(t, err) @@ -25,7 +25,7 @@ func TestAA55UDPFromConfig_BlockRejectsOutOfRange(t *testing.T) { _, err := NewAA55UDPFromConfig(context.Background(), map[string]any{ "host": "127.0.0.1", "id": 247, - "register": map[string]any{"address": 36017, "decode": "float32be"}, // outside READ 125 @ 35100 + "register": map[string]any{"address": 36017, "decode": "float32"}, // outside READ 125 @ 35100 "block": map[string]any{"register": 35100, "count": 125}, }) require.Error(t, err) @@ -35,7 +35,7 @@ func TestAA55UDPFromConfig_BlockRejectsOutOfRange(t *testing.T) { func TestAA55UDPFromConfig_Register(t *testing.T) { _, err := NewAA55UDPFromConfig(context.Background(), map[string]any{ "host": "127.0.0.1", - "register": map[string]any{"address": 30127, "decode": "int32be"}, + "register": map[string]any{"address": 30127, "decode": "int32"}, }) require.NoError(t, err) } diff --git a/templates/definition/meter/goodwe-wifi-dt.yaml b/templates/definition/meter/goodwe-wifi-dt.yaml index 9785b2205..6f10bfb51 100644 --- a/templates/definition/meter/goodwe-wifi-dt.yaml +++ b/templates/definition/meter/goodwe-wifi-dt.yaml @@ -16,11 +16,11 @@ render: | host: {{ .host }} register: address: 30127 - decode: int32be + decode: int32 energy: source: aa55udp host: {{ .host }} register: address: 30145 - decode: uint32be + decode: uint32 scale: 0.1 diff --git a/templates/definition/meter/goodwe-wifi-es.yaml b/templates/definition/meter/goodwe-wifi-es.yaml index 9cab17415..556f9c750 100644 --- a/templates/definition/meter/goodwe-wifi-es.yaml +++ b/templates/definition/meter/goodwe-wifi-es.yaml @@ -19,7 +19,7 @@ render: | host: {{ or .host .uri }} register: address: 29964 - decode: int32be + decode: int32 {{- end }} {{- if eq .usage "pv" }} power: @@ -27,7 +27,7 @@ render: | host: {{ or .host .uri }} register: address: 29958 - decode: int32be + decode: int32 {{- end }} {{- if eq .usage "battery" }} power: @@ -35,11 +35,11 @@ render: | host: {{ or .host .uri }} register: address: 29970 - decode: int32be + decode: int32 soc: source: aa55udp host: {{ or .host .uri }} register: address: 29966 - decode: uint16be + decode: uint16 {{- end }} diff --git a/templates/definition/meter/goodwe-wifi-et.yaml b/templates/definition/meter/goodwe-wifi-et.yaml index 99b5fca3f..63089aa4f 100644 --- a/templates/definition/meter/goodwe-wifi-et.yaml +++ b/templates/definition/meter/goodwe-wifi-et.yaml @@ -27,7 +27,7 @@ render: | count: 125 register: address: 35139 # grid power - decode: int32be + decode: int32 delay: {{ .delay }} energy: source: aa55udp @@ -35,7 +35,7 @@ render: | id: 247 register: address: 36017 - decode: float32be + decode: float32 scale: 0.001 delay: {{ .delay }} {{- end }} @@ -92,7 +92,7 @@ render: | count: 125 register: address: 35191 # pv energy - decode: uint32be + decode: uint32 scale: 0.1 delay: {{ .delay }} {{- end }} @@ -106,7 +106,7 @@ render: | count: 125 register: address: 35182 # battery power - decode: int32be + decode: int32 delay: {{ .delay }} energy: source: aa55udp @@ -117,7 +117,7 @@ render: | count: 125 register: address: 35209 # battery discharge energy - decode: uint32be + decode: uint32 scale: 0.1 delay: {{ .delay }} soc: @@ -129,6 +129,6 @@ render: | count: 13 register: address: 37007 # soc - decode: uint16be + decode: uint16 delay: {{ .delay }} {{- end }}