Files
invertergui/mk2driver/mk2_test.go
Nathan Coad e8153e2953
All checks were successful
continuous-integration/drone/push Build is passing
implement some features of Venus OS
2026-02-19 15:37:41 +11:00

1026 lines
28 KiB
Go

package mk2driver
import (
"bytes"
"io"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
var knownWrites = []byte{
0x04, 0xff, 0x41, 0x01, 0x00, 0xbb,
0x05, 0xff, 0x57, 0x36, 0x00, 0x00, 0x6f,
0x05, 0xff, 0x57, 0x36, 0x01, 0x00, 0x6e,
0x05, 0xff, 0x57, 0x36, 0x02, 0x00, 0x6d,
0x05, 0xff, 0x57, 0x36, 0x03, 0x00, 0x6c,
0x05, 0xff, 0x57, 0x36, 0x04, 0x00, 0x6b,
0x05, 0xff, 0x57, 0x36, 0x05, 0x00, 0x6a,
0x05, 0xff, 0x57, 0x36, 0x06, 0x00, 0x69,
0x05, 0xff, 0x57, 0x36, 0x07, 0x00, 0x68,
0x05, 0xff, 0x57, 0x36, 0x08, 0x00, 0x67,
0x05, 0xff, 0x57, 0x36, 0x09, 0x00, 0x66,
0x05, 0xff, 0x57, 0x36, 0x0a, 0x00, 0x65,
0x05, 0xff, 0x57, 0x36, 0x0b, 0x00, 0x64,
0x05, 0xff, 0x57, 0x36, 0x0c, 0x00, 0x63,
0x05, 0xff, 0x57, 0x36, 0x0d, 0x00, 0x62,
0x03, 0xff, 0x46, 0x00, 0xb8,
0x03, 0xff, 0x46, 0x01, 0xb7,
0x02, 0xff, 0x4c, 0xb3,
0x05, 0xff, 0x57, 0x30, 0x0d, 0x00, 0x68,
}
var writeBuffer = bytes.NewBuffer(nil)
const (
testDelta = 0.00000001
)
type testIo struct {
io.Reader
io.Writer
}
func NewIOStub(readBuffer []byte) io.ReadWriter {
writeBuffer = bytes.NewBuffer(nil)
return &testIo{
Reader: bytes.NewBuffer(readBuffer),
Writer: writeBuffer,
}
}
func buildTestFrame(frameType byte, payload ...byte) (byte, []byte) {
length := byte(len(payload) + 1)
sum := int(length) + int(frameType)
for _, b := range payload {
sum += int(b)
}
checksum := byte((-sum) & 0xff)
frame := append([]byte{frameType}, payload...)
frame = append(frame, checksum)
return length, frame
}
func buildSentCommand(payload ...byte) []byte {
length := byte(len(payload) + 1)
sum := int(length) + int(frameHeader)
out := make([]byte, 0, len(payload)+3)
out = append(out, length, frameHeader)
for _, b := range payload {
sum += int(b)
out = append(out, b)
}
out = append(out, byte((-sum)&0xff))
return out
}
// Test a know sequence as reference as extracted from Mk2
func TestSync(t *testing.T) {
tests := []struct {
name string
knownReadBuffer []byte
knownWrites []byte
result Mk2Info
}{
{
name: "basic",
knownReadBuffer: []byte{
//Len Cmd
0x04, 0xff, 0x41, 0x01, 0x00, 0xbb,
0x07, 0xff, 0x56, 0x96, 0x3e, 0x11, 0x00, 0x00, 0xbf,
0x08, 0xff, 0x57, 0x8e, 0x9c, 0x7f, 0x8f, 0x00, 0x00, 0x6a,
0x08, 0xff, 0x57, 0x8e, 0x64, 0x80, 0x8f, 0x00, 0x00, 0xa1,
0x08, 0xff, 0x57, 0x8e, 0x9c, 0x7f, 0x8f, 0x00, 0x00, 0x6a,
0x08, 0xff, 0x57, 0x8e, 0x9c, 0x7f, 0x8f, 0x00, 0x00, 0x6a,
0x08, 0xff, 0x57, 0x8e, 0x9c, 0x7f, 0x8f, 0x00, 0x00, 0x6a,
0x08, 0xff, 0x57, 0x8e, 0x64, 0x80, 0x8f, 0x00, 0x00, 0xa1,
0x08, 0xff, 0x57, 0x8e, 0x9c, 0x7f, 0x8f, 0x00, 0x00, 0x6a,
0x08, 0xff, 0x57, 0x8e, 0x57, 0x78, 0x8f, 0x00, 0x01, 0xb5,
0x08, 0xff, 0x57, 0x8e, 0x2f, 0x7c, 0x8f, 0x00, 0x00, 0xda,
0x08, 0xff, 0x57, 0x8e, 0x64, 0x80, 0x8f, 0x00, 0x00, 0xa1,
0x08, 0xff, 0x57, 0x8e, 0x04, 0x00, 0x8f, 0x00, 0x80, 0x01,
0x08, 0xff, 0x57, 0x8e, 0x01, 0x00, 0x8f, 0x00, 0x80, 0x04,
0x08, 0xff, 0x57, 0x8e, 0x02, 0x00, 0x8f, 0x00, 0x80, 0x03,
0x08, 0xff, 0x57, 0x8e, 0x38, 0x7f, 0x8f, 0x00, 0x00, 0xce,
0x07, 0xff, 0x56, 0x96, 0x3e, 0x11, 0x00, 0x00, 0xbf,
0x0f, 0x20, 0xf3, 0x00, 0xc8, 0x02, 0x0c, 0xa1, 0x05, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x88, 0xb2,
0x0f, 0x20, 0x01, 0x01, 0xca, 0x09, 0x08, 0xaa, 0x58, 0xab, 0x00, 0xaa, 0x58, 0x9a, 0x00, 0xc3, 0xe8,
0x06, 0xff, 0x4c, 0x03, 0x00, 0x00, 0x00, 0xac,
0x05, 0xff, 0x57, 0x85, 0xc8, 0x00, 0x58,
},
knownWrites: []byte{
0x04, 0xff, 0x41, 0x01, 0x00, 0xbb,
0x05, 0xff, 0x57, 0x36, 0x00, 0x00, 0x6f,
0x05, 0xff, 0x57, 0x36, 0x01, 0x00, 0x6e,
0x05, 0xff, 0x57, 0x36, 0x02, 0x00, 0x6d,
0x05, 0xff, 0x57, 0x36, 0x03, 0x00, 0x6c,
0x05, 0xff, 0x57, 0x36, 0x04, 0x00, 0x6b,
0x05, 0xff, 0x57, 0x36, 0x05, 0x00, 0x6a,
0x05, 0xff, 0x57, 0x36, 0x06, 0x00, 0x69,
0x05, 0xff, 0x57, 0x36, 0x07, 0x00, 0x68,
0x05, 0xff, 0x57, 0x36, 0x08, 0x00, 0x67,
0x05, 0xff, 0x57, 0x36, 0x09, 0x00, 0x66,
0x05, 0xff, 0x57, 0x36, 0x0a, 0x00, 0x65,
0x05, 0xff, 0x57, 0x36, 0x0b, 0x00, 0x64,
0x05, 0xff, 0x57, 0x36, 0x0c, 0x00, 0x63,
0x05, 0xff, 0x57, 0x36, 0x0d, 0x00, 0x62,
0x03, 0xff, 0x46, 0x00, 0xb8,
0x03, 0xff, 0x46, 0x01, 0xb7,
0x02, 0xff, 0x4c, 0xb3,
0x05, 0xff, 0x57, 0x30, 0x0d, 0x00, 0x68,
},
result: Mk2Info{
Version: uint32(2736),
BatVoltage: 14.41,
BatCurrent: -0.4,
InVoltage: 226.98,
InCurrent: 1.71,
InFrequency: 50.10256410256411,
OutVoltage: 226.980,
OutCurrent: 1.54,
OutFrequency: 50.025510204081634,
ChargeState: 1,
LEDs: map[Led]LEDstate{
LedMain: LedOn,
LedAbsorption: LedOn,
LedBulk: LedOff,
LedFloat: LedOff,
LedInverter: LedOff,
LedOverload: LedOff,
LedLowBattery: LedOff,
LedTemperature: LedOff,
},
},
},
{
name: "multiplus24/3000",
knownReadBuffer: []byte{
//Len Cmd
0x04, 0xff, 0x41, 0x01, 0x00, 0xbb,
0x07, 0xff, 0x56, 0x98, 0x3e, 0x11, 0x00, 0x00, 0xbd, // version
0x08, 0xff, 0x57, 0x8e, 0x9c, 0x7f, 0x8f, 0x0, 0x0, 0x6a, // scale 0
0x08, 0xff, 0x57, 0x8e, 0x9c, 0x7f, 0x8f, 0x0, 0x0, 0x6a, // scale 1
0x08, 0xff, 0x57, 0x8e, 0x9c, 0x7f, 0x8f, 0x0, 0x0, 0x6a, // scale 2
0x08, 0xff, 0x57, 0x8e, 0x9c, 0x7f, 0x8f, 0x0, 0x0, 0x6a, // scale 3
0x08, 0xff, 0x57, 0x8e, 0x9c, 0x7f, 0x8f, 0x0, 0x0, 0x6a, // scale 4
0x08, 0xff, 0x57, 0x8e, 0x64, 0x80, 0x8f, 0x0, 0x0, 0xa1, // scale 5
0x08, 0xff, 0x57, 0x8e, 0x9c, 0x7f, 0x8f, 0x0, 0x0, 0x6a, // scale 6
0x08, 0xff, 0x57, 0x8e, 0x57, 0x78, 0x8f, 0x0, 0x1, 0xb5, // scale 7
0x08, 0xff, 0x57, 0x8e, 0x2f, 0x7c, 0x8f, 0x0, 0x0, 0xda, // scale 8
0x08, 0xff, 0x57, 0x8e, 0x64, 0x80, 0x8f, 0x0, 0x0, 0xa1, //scale 9
0x08, 0xff, 0x57, 0x8e, 0x4, 0x0, 0x8f, 0x0, 0x80, 0x1, // scale 10
0x08, 0xff, 0x57, 0x8e, 0x1, 0x0, 0x8f, 0x0, 0x80, 0x4, // scale 11
0x08, 0xff, 0x57, 0x8e, 0x6, 0x0, 0x8f, 0x0, 0x80, 0xff, // scale 12
0x08, 0xff, 0x57, 0x8e, 0x38, 0x7f, 0x8f, 0x0, 0x0, 0xce, // scale 13
0x07, 0xff, 0x56, 0x98, 0x3e, 0x11, 0x0, 0x0, 0xbd, // version
0x0f, 0x20, 0xb6, 0x89, 0x6d, 0xb7, 0xc, 0x4e, 0xa, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x88, 0x82, // dc info
0x0f, 0x20, 0x1, 0x1, 0x6d, 0xb7, 0x8, 0x77, 0x5b, 0x21, 0x0, 0x77, 0x5b, 0xfe, 0xff, 0xc3, 0x1e, // ac info
0x08, 0xff, 0x4c, 0x9, 0x0, 0x0, 0x0, 0x3, 0x0, 0xa1,
0x05, 0xff, 0x57, 0x85, 0xc8, 0x0, 0x58,
},
knownWrites: []byte{},
result: Mk2Info{
Version: 0xac0,
BatVoltage: 26.38,
BatCurrent: 0,
InVoltage: 234.15,
InCurrent: 0.33,
InFrequency: 50.1025641025641,
OutVoltage: 234.15,
OutCurrent: -0.02,
OutFrequency: 50.025510204081634,
ChargeState: 1,
LEDs: map[Led]LEDstate{
LedMain: LedOn,
LedAbsorption: LedOff,
LedBulk: LedOff,
LedFloat: LedOn,
LedInverter: LedOff,
LedOverload: LedOff,
LedLowBattery: LedOff,
LedTemperature: LedOff,
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
testIO := NewIOStub(tt.knownReadBuffer)
mk2, err := NewMk2Connection(testIO)
assert.NoError(t, err, "Could not open MK2")
event := <-mk2.C()
mk2.Close()
if len(tt.knownWrites) > 0 {
assert.Equal(t, 0, bytes.Compare(writeBuffer.Bytes(), knownWrites), "Expected writes did not match received writes")
}
assert.True(t, event.Valid, "data not valid")
assert.Equal(t, tt.result.Version, event.Version, "Invalid version decoded")
assert.Equal(t, 0, len(event.Errors), "Reported errors not empty")
assert.Equal(t, tt.result.LEDs, event.LEDs, "Reported LEDs incorrect")
assert.InDelta(t, tt.result.BatVoltage, event.BatVoltage, testDelta, "BatVoltage conversion failed")
assert.InDelta(t, tt.result.BatCurrent, event.BatCurrent, testDelta, "BatCurrent conversion failed")
assert.InDelta(t, tt.result.InVoltage, event.InVoltage, testDelta, "InVoltage conversion failed")
assert.InDelta(t, tt.result.InCurrent, event.InCurrent, testDelta, "InCurrent conversion failed")
assert.InDelta(t, tt.result.InFrequency, event.InFrequency, testDelta, "InFrequency conversion failed")
assert.InDelta(t, tt.result.OutVoltage, event.OutVoltage, testDelta, "OutVoltage conversion failed")
assert.InDelta(t, tt.result.OutCurrent, event.OutCurrent, testDelta, "OutCurrent conversion failed")
assert.InDelta(t, tt.result.OutFrequency, event.OutFrequency, testDelta, "OutFrequency conversion failed")
assert.InDelta(t, tt.result.ChargeState, event.ChargeState, testDelta, "ChargeState conversion failed")
})
}
}
func Test_mk2Ser_scaleDecode(t *testing.T) {
tests := []struct {
name string
frame []byte
expectedScaling scaling
}{
{
name: "Valid scale",
frame: []byte{0x8e, 0x9c, 0x7f, 0x8f, 0x01, 0x00, 0x6a},
expectedScaling: scaling{
scale: 0.01,
offset: 1,
supported: true,
},
},
{
name: "Unsupported frame",
frame: []byte{0x00},
expectedScaling: scaling{
supported: false,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
m := &mk2Ser{
scales: make([]scaling, 0, ramVarMaxOffset),
p: NewIOStub([]byte{}),
}
m.scaleDecode(tt.frame)
assert.Equal(t, 1, len(m.scales))
assert.Equal(t, 1, m.scaleCount)
assert.Equal(t, tt.expectedScaling.supported, m.scales[0].supported)
assert.Equal(t, tt.expectedScaling.signed, m.scales[0].signed)
if tt.expectedScaling.supported {
assert.InDelta(t, tt.expectedScaling.offset, m.scales[0].offset, testDelta)
assert.InDelta(t, tt.expectedScaling.scale, m.scales[0].scale, testDelta)
}
})
}
}
func Test_mk2Ser_calcFreq(t *testing.T) {
tests := []struct {
name string
scales []scaling
data byte
scaleIndex int
want float64
}{
{
name: "Calculate working low",
scales: []scaling{
{supported: false},
},
data: 0x01,
scaleIndex: 0,
want: 10,
},
{
name: "Calculate working high",
scales: []scaling{
{
supported: true,
offset: 0,
scale: 0.01,
},
},
data: 0xFE,
scaleIndex: 0,
want: 3.937007874015748,
},
{
name: "Calculate clip high",
scales: []scaling{
{supported: false},
},
data: 0xff,
scaleIndex: 0,
want: 0,
},
{
name: "Calculate clip low",
scales: []scaling{
{supported: false},
},
data: 0x00,
scaleIndex: 0,
want: 0,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
m := &mk2Ser{
scales: tt.scales,
}
got := m.calcFreq(tt.data, tt.scaleIndex)
assert.InDelta(t, tt.want, got, testDelta)
})
}
}
func Test_mk2Ser_WriteSetting(t *testing.T) {
testIO := NewIOStub(nil)
m := &mk2Ser{
p: testIO,
writeAck: make(chan byte, 1),
}
go func() {
time.Sleep(10 * time.Millisecond)
m.pushWriteResponse(commandWriteViaIDResponse)
}()
err := m.WriteSetting(0x1234, 1234)
assert.NoError(t, err)
expected := []byte{
0x07, 0xff, 0x57, 0x37, 0x34, 0x12, 0xd2, 0x04, 0x50,
}
assert.Equal(t, expected, writeBuffer.Bytes())
}
func Test_mk2Ser_WriteSetting_FallbackLegacy(t *testing.T) {
testIO := NewIOStub(nil)
m := &mk2Ser{
p: testIO,
writeAck: make(chan byte, 2),
}
go func() {
time.Sleep(10 * time.Millisecond)
m.pushWriteResponse(commandUnsupportedResponse)
time.Sleep(10 * time.Millisecond)
m.pushWriteResponse(commandWriteSettingResponse)
}()
err := m.WriteSetting(0x1234, 1234)
assert.NoError(t, err)
expected := []byte{
0x07, 0xff, 0x57, 0x37, 0x34, 0x12, 0xd2, 0x04, 0x50,
0x05, 0xff, 0x57, 0x33, 0x34, 0x12, 0x2c,
0x05, 0xff, 0x57, 0x34, 0xd2, 0x04, 0x9b,
}
assert.Equal(t, expected, writeBuffer.Bytes())
}
func Test_mk2Ser_WriteRAMVar(t *testing.T) {
testIO := NewIOStub(nil)
m := &mk2Ser{
p: testIO,
writeAck: make(chan byte, 1),
}
go func() {
time.Sleep(10 * time.Millisecond)
m.pushWriteResponse(commandWriteRAMViaIDResponse)
}()
err := m.WriteRAMVar(0x000d, 1)
assert.NoError(t, err)
expected := buildSentCommand(winmonFrame, commandWriteRAMViaID, 0x0d, 0x00, 0x01, 0x00)
assert.Equal(t, expected, writeBuffer.Bytes())
}
func Test_mk2Ser_WriteRAMVar_FallbackLegacy(t *testing.T) {
testIO := NewIOStub(nil)
m := &mk2Ser{
p: testIO,
writeAck: make(chan byte, 2),
}
go func() {
time.Sleep(10 * time.Millisecond)
m.pushWriteResponse(commandUnsupportedResponse)
time.Sleep(10 * time.Millisecond)
m.pushWriteResponse(commandWriteRAMResponse)
}()
err := m.WriteRAMVar(0x000d, 1)
assert.NoError(t, err)
expected := append([]byte{}, buildSentCommand(winmonFrame, commandWriteRAMViaID, 0x0d, 0x00, 0x01, 0x00)...)
expected = append(expected, buildSentCommand(winmonFrame, commandWriteRAMVar, 0x0d, 0x00)...)
expected = append(expected, buildSentCommand(winmonFrame, commandWriteData, 0x01, 0x00)...)
assert.Equal(t, expected, writeBuffer.Bytes())
}
func Test_mk2Ser_WriteSettingByID(t *testing.T) {
testIO := NewIOStub(nil)
m := &mk2Ser{
p: testIO,
writeAck: make(chan byte, 1),
}
go func() {
time.Sleep(10 * time.Millisecond)
m.pushWriteResponse(commandWriteViaIDResponse)
}()
err := m.WriteSettingByID(0x1234, 1234)
assert.NoError(t, err)
expected := buildSentCommand(winmonFrame, commandWriteViaID, 0x34, 0x12, 0xd2, 0x04)
assert.Equal(t, expected, writeBuffer.Bytes())
}
func Test_mk2Ser_WriteRAMVarByID(t *testing.T) {
testIO := NewIOStub(nil)
m := &mk2Ser{
p: testIO,
writeAck: make(chan byte, 1),
}
go func() {
time.Sleep(10 * time.Millisecond)
m.pushWriteResponse(commandWriteRAMViaIDResponse)
}()
err := m.WriteRAMVarByID(0x000d, 1)
assert.NoError(t, err)
expected := buildSentCommand(winmonFrame, commandWriteRAMViaID, 0x0d, 0x00, 0x01, 0x00)
assert.Equal(t, expected, writeBuffer.Bytes())
}
func Test_mk2Ser_RegisterMetadata(t *testing.T) {
m := &mk2Ser{}
meta, ok := m.RegisterMetadata(RegisterKindRAMVar, ramVarVBat)
assert.True(t, ok)
assert.Equal(t, RegisterKindRAMVar, meta.Kind)
assert.Equal(t, uint16(ramVarVBat), meta.ID)
assert.Equal(t, "battery_voltage", meta.Name)
assert.False(t, meta.Writable)
_, ok = m.RegisterMetadata(RegisterKindSetting, 9999)
assert.False(t, ok)
all := m.ListRegisterMetadata()
assert.NotEmpty(t, all)
}
func Test_mk2Ser_WriteRegister_Verified(t *testing.T) {
testIO := NewIOStub(nil)
m := &mk2Ser{
p: testIO,
writeAck: make(chan byte, 2),
winmonAck: make(chan winmonResponse, 4),
}
go func() {
time.Sleep(10 * time.Millisecond)
// Read-before-write response.
m.pushWinmonResponse(commandReadSettingResponse, []byte{0x01, 0x00})
time.Sleep(10 * time.Millisecond)
// Write acknowledgement.
m.pushWriteResponse(commandWriteViaIDResponse)
time.Sleep(10 * time.Millisecond)
// Verify-after-write response.
m.pushWinmonResponse(commandReadSettingResponse, []byte{0x34, 0x12})
}()
result, err := m.WriteRegister(RegisterKindSetting, 0x0042, 0x1234, TransactionOptions{
ReadBeforeWrite: true,
VerifyAfterWrite: true,
})
assert.NoError(t, err)
assert.Equal(t, 1, result.Attempts)
if assert.NotNil(t, result.PreviousValue) {
assert.Equal(t, int16(1), *result.PreviousValue)
}
if assert.NotNil(t, result.VerifiedValue) {
assert.Equal(t, int16(0x1234), *result.VerifiedValue)
}
expected := append([]byte{}, buildSentCommand(winmonFrame, commandReadSetting, 0x42, 0x00)...)
expected = append(expected, buildSentCommand(winmonFrame, commandWriteViaID, 0x42, 0x00, 0x34, 0x12)...)
expected = append(expected, buildSentCommand(winmonFrame, commandReadSetting, 0x42, 0x00)...)
assert.Equal(t, expected, writeBuffer.Bytes())
}
func Test_mk2Ser_WriteRegister_VerifyRetry(t *testing.T) {
testIO := NewIOStub(nil)
m := &mk2Ser{
p: testIO,
writeAck: make(chan byte, 4),
winmonAck: make(chan winmonResponse, 8),
}
go func() {
time.Sleep(10 * time.Millisecond)
m.pushWriteResponse(commandWriteViaIDResponse)
time.Sleep(10 * time.Millisecond)
// First verify mismatch.
m.pushWinmonResponse(commandReadSettingResponse, []byte{0x00, 0x00})
time.Sleep(10 * time.Millisecond)
m.pushWriteResponse(commandWriteViaIDResponse)
time.Sleep(10 * time.Millisecond)
// Second verify matches expected value.
m.pushWinmonResponse(commandReadSettingResponse, []byte{0x78, 0x56})
}()
result, err := m.WriteRegister(RegisterKindSetting, 0x0042, 0x5678, TransactionOptions{
Retries: 1,
RetryDelay: 1 * time.Millisecond,
VerifyAfterWrite: true,
})
assert.NoError(t, err)
assert.Equal(t, 2, result.Attempts)
if assert.NotNil(t, result.VerifiedValue) {
assert.Equal(t, int16(0x5678), *result.VerifiedValue)
}
}
func Test_mk2Ser_WriteRegister_ReadOnlyMetadata(t *testing.T) {
testIO := NewIOStub(nil)
m := &mk2Ser{
p: testIO,
writeAck: make(chan byte, 1),
}
_, err := m.WriteRegister(RegisterKindRAMVar, ramVarVBat, 1, TransactionOptions{
VerifyAfterWrite: true,
})
assert.Error(t, err)
assert.ErrorContains(t, err, "read-only")
assert.Empty(t, writeBuffer.Bytes())
}
func Test_mk2Ser_GetDeviceState(t *testing.T) {
testIO := NewIOStub(nil)
m := &mk2Ser{
p: testIO,
winmonAck: make(chan winmonResponse, 1),
}
go func() {
time.Sleep(10 * time.Millisecond)
m.pushWinmonResponse(commandSetStateResponse, []byte{0x00, byte(DeviceStateOn)})
}()
state, err := m.GetDeviceState()
assert.NoError(t, err)
assert.Equal(t, DeviceStateOn, state)
expected := buildSentCommand(winmonFrame, commandSetState, 0x00)
assert.Equal(t, expected, writeBuffer.Bytes())
}
func Test_mk2Ser_SetDeviceState(t *testing.T) {
testIO := NewIOStub(nil)
m := &mk2Ser{
p: testIO,
winmonAck: make(chan winmonResponse, 1),
}
go func() {
time.Sleep(10 * time.Millisecond)
m.pushWinmonResponse(commandSetStateResponse, []byte{0x00, byte(DeviceStateOff)})
}()
err := m.SetDeviceState(DeviceStateOff)
assert.NoError(t, err)
expected := buildSentCommand(winmonFrame, commandSetState, 0x00, byte(DeviceStateOff))
assert.Equal(t, expected, writeBuffer.Bytes())
}
func Test_mk2Ser_ReadRAMVarByID(t *testing.T) {
testIO := NewIOStub(nil)
m := &mk2Ser{
p: testIO,
winmonAck: make(chan winmonResponse, 1),
}
go func() {
time.Sleep(10 * time.Millisecond)
m.pushWinmonResponse(commandReadRAMResponse, []byte{0x34, 0x12})
}()
value, err := m.ReadRAMVarByID(0x0021)
assert.NoError(t, err)
assert.Equal(t, int16(0x1234), value)
expected := buildSentCommand(winmonFrame, commandReadRAMVar, 0x21, 0x00)
assert.Equal(t, expected, writeBuffer.Bytes())
}
func Test_mk2Ser_ReadSettingByID(t *testing.T) {
testIO := NewIOStub(nil)
m := &mk2Ser{
p: testIO,
winmonAck: make(chan winmonResponse, 1),
}
go func() {
time.Sleep(10 * time.Millisecond)
m.pushWinmonResponse(commandReadSettingResponse, []byte{0xcd, 0xab})
}()
value, err := m.ReadSettingByID(0x0042)
assert.NoError(t, err)
assert.Equal(t, int16(-21555), value)
expected := buildSentCommand(winmonFrame, commandReadSetting, 0x42, 0x00)
assert.Equal(t, expected, writeBuffer.Bytes())
}
func Test_mk2Ser_ReadSelected(t *testing.T) {
testIO := NewIOStub(nil)
m := &mk2Ser{
p: testIO,
winmonAck: make(chan winmonResponse, 1),
}
go func() {
time.Sleep(10 * time.Millisecond)
m.pushWinmonResponse(commandReadSelectedResponse, []byte{0x78, 0x56})
}()
value, err := m.ReadSelected()
assert.NoError(t, err)
assert.Equal(t, int16(0x5678), value)
expected := buildSentCommand(winmonFrame, commandReadSelected)
assert.Equal(t, expected, writeBuffer.Bytes())
}
func Test_mk2Ser_SelectRAMVar(t *testing.T) {
testIO := NewIOStub(nil)
m := &mk2Ser{
p: testIO,
winmonAck: make(chan winmonResponse, 1),
}
go func() {
time.Sleep(10 * time.Millisecond)
m.pushWinmonResponse(commandReadRAMResponse, []byte{0x11, 0x22})
}()
err := m.SelectRAMVar(0x0022)
assert.NoError(t, err)
expected := buildSentCommand(winmonFrame, commandReadRAMVar, 0x22, 0x00)
assert.Equal(t, expected, writeBuffer.Bytes())
}
func Test_mk2Ser_SelectSetting(t *testing.T) {
testIO := NewIOStub(nil)
m := &mk2Ser{
p: testIO,
winmonAck: make(chan winmonResponse, 1),
}
go func() {
time.Sleep(10 * time.Millisecond)
m.pushWinmonResponse(commandReadSettingResponse, []byte{0x11, 0x22})
}()
err := m.SelectSetting(0x0023)
assert.NoError(t, err)
expected := buildSentCommand(winmonFrame, commandReadSetting, 0x23, 0x00)
assert.Equal(t, expected, writeBuffer.Bytes())
}
func Test_mk2Ser_ReadRAMVarInfo(t *testing.T) {
testIO := NewIOStub(nil)
m := &mk2Ser{
p: testIO,
winmonAck: make(chan winmonResponse, 1),
}
go func() {
time.Sleep(10 * time.Millisecond)
m.pushWinmonResponse(commandGetRAMVarInfoResponse, []byte{0x9c, 0x7f, 0x00, 0x8f, 0x00})
}()
info, err := m.ReadRAMVarInfo(0x0001)
assert.NoError(t, err)
assert.True(t, info.Supported)
assert.Equal(t, uint16(0x0001), info.ID)
assert.Equal(t, int16(0x7f9c), info.Scale)
assert.Equal(t, int16(0x008f), info.Offset)
assert.InDelta(t, 0.01, info.Factor, testDelta)
expected := buildSentCommand(winmonFrame, commandGetRAMVarInfo, 0x01, 0x00)
assert.Equal(t, expected, writeBuffer.Bytes())
}
func Test_mk2Ser_WriteRAMVarRejected(t *testing.T) {
testIO := NewIOStub(nil)
m := &mk2Ser{
p: testIO,
writeAck: make(chan byte, 1),
}
go func() {
time.Sleep(10 * time.Millisecond)
m.pushWriteResponse(commandWriteNotAllowedResponse)
}()
err := m.WriteRAMVar(0x000d, 1)
assert.Error(t, err)
assert.ErrorContains(t, err, "rejected")
}
func Test_mk2Ser_SetPanelState(t *testing.T) {
testIO := NewIOStub(nil)
m := &mk2Ser{
p: testIO,
stateAck: make(chan struct{}, 1),
}
go func() {
time.Sleep(10 * time.Millisecond)
m.pushStateResponse()
}()
currentLimit := 16.5
err := m.SetPanelState(PanelSwitchOn, &currentLimit)
assert.NoError(t, err)
expected := []byte{
0x07, 0xff, 0x53, 0x03, 0xa5, 0x00, 0x01, 0x80, 0x7e,
}
assert.Equal(t, expected, writeBuffer.Bytes())
}
func Test_mk2Ser_SetPanelState_SwitchOnly(t *testing.T) {
testIO := NewIOStub(nil)
m := &mk2Ser{
p: testIO,
stateAck: make(chan struct{}, 1),
}
go func() {
time.Sleep(10 * time.Millisecond)
m.pushStateResponse()
}()
err := m.SetPanelState(PanelSwitchOff, nil)
assert.NoError(t, err)
expected := []byte{
0x07, 0xff, 0x53, 0x04, 0x00, 0x80, 0x01, 0x80, 0xa2,
}
assert.Equal(t, expected, writeBuffer.Bytes())
}
func Test_mk2Ser_SetStandby(t *testing.T) {
testIO := NewIOStub(nil)
m := &mk2Ser{
p: testIO,
ifaceAck: make(chan byte, 1),
}
go func() {
time.Sleep(10 * time.Millisecond)
m.pushInterfaceResponse(interfacePanelDetectFlag | interfaceStandbyFlag)
}()
err := m.SetStandby(true)
assert.NoError(t, err)
expected := []byte{
0x03, 0xff, 0x48, 0x03, 0xb3,
}
assert.Equal(t, expected, writeBuffer.Bytes())
}
func Test_mk2Ser_SetStandby_Disabled(t *testing.T) {
testIO := NewIOStub(nil)
m := &mk2Ser{
p: testIO,
ifaceAck: make(chan byte, 1),
}
go func() {
time.Sleep(10 * time.Millisecond)
m.pushInterfaceResponse(interfacePanelDetectFlag)
}()
err := m.SetStandby(false)
assert.NoError(t, err)
expected := []byte{
0x03, 0xff, 0x48, 0x01, 0xb5,
}
assert.Equal(t, expected, writeBuffer.Bytes())
}
func Test_mk2Ser_WriteSettingBySelection(t *testing.T) {
testIO := NewIOStub(nil)
m := &mk2Ser{
p: testIO,
writeAck: make(chan byte, 1),
}
go func() {
time.Sleep(10 * time.Millisecond)
m.pushWriteResponse(commandWriteSettingResponse)
}()
err := m.WriteSettingBySelection(0x0020, 0x0011)
assert.NoError(t, err)
expected := append([]byte{}, buildSentCommand(winmonFrame, commandWriteSetting, 0x20, 0x00)...)
expected = append(expected, buildSentCommand(winmonFrame, commandWriteData, 0x11, 0x00)...)
assert.Equal(t, expected, writeBuffer.Bytes())
}
func Test_mk2Ser_WriteSelectedData(t *testing.T) {
testIO := NewIOStub(nil)
m := &mk2Ser{
p: testIO,
writeAck: make(chan byte, 1),
}
go func() {
time.Sleep(10 * time.Millisecond)
m.pushWriteResponse(commandWriteRAMResponse)
}()
err := m.WriteSelectedData(0x0022)
assert.NoError(t, err)
expected := buildSentCommand(winmonFrame, commandWriteData, 0x22, 0x00)
assert.Equal(t, expected, writeBuffer.Bytes())
}
func Test_mk2Ser_CaptureAndDiffSnapshot(t *testing.T) {
testIO := NewIOStub(nil)
m := &mk2Ser{
p: testIO,
winmonAck: make(chan winmonResponse, 2),
}
go func() {
time.Sleep(10 * time.Millisecond)
m.pushWinmonResponse(commandReadRAMResponse, []byte{0x03, 0x00})
time.Sleep(10 * time.Millisecond)
m.pushWinmonResponse(commandReadRAMResponse, []byte{0x04, 0x00})
}()
addresses := []RegisterAddress{
{Kind: RegisterKindRAMVar, ID: ramVarVirSwitchPos},
}
snapshot, err := m.CaptureSnapshot(addresses)
assert.NoError(t, err)
if assert.Len(t, snapshot.Entries, 1) {
assert.Equal(t, RegisterSafetyGuarded, snapshot.Entries[0].Safety)
assert.Equal(t, int16(3), snapshot.Entries[0].Value)
}
diffs, err := m.DiffSnapshot(snapshot)
assert.NoError(t, err)
if assert.Len(t, diffs, 1) {
assert.True(t, diffs[0].Changed)
assert.Equal(t, int16(4), diffs[0].Current)
assert.Equal(t, int16(3), diffs[0].Target)
}
}
func Test_mk2Ser_RestoreSnapshot(t *testing.T) {
testIO := NewIOStub(nil)
m := &mk2Ser{
p: testIO,
winmonAck: make(chan winmonResponse, 4),
writeAck: make(chan byte, 2),
}
go func() {
time.Sleep(10 * time.Millisecond)
// DiffSnapshot current value.
m.pushWinmonResponse(commandReadRAMResponse, []byte{0x04, 0x00})
time.Sleep(10 * time.Millisecond)
// WriteRegister read-before-write.
m.pushWinmonResponse(commandReadRAMResponse, []byte{0x04, 0x00})
time.Sleep(10 * time.Millisecond)
// Write ack.
m.pushWriteResponse(commandWriteRAMViaIDResponse)
time.Sleep(10 * time.Millisecond)
// Verify-after-write.
m.pushWinmonResponse(commandReadRAMResponse, []byte{0x03, 0x00})
}()
result, err := m.RestoreSnapshot(RegisterSnapshot{
Entries: []RegisterSnapshotEntry{
{
Kind: RegisterKindRAMVar,
ID: ramVarVirSwitchPos,
Value: 3,
Writable: true,
},
},
}, TransactionOptions{
ReadBeforeWrite: true,
VerifyAfterWrite: true,
RetryDelay: 1 * time.Millisecond,
})
assert.NoError(t, err)
assert.False(t, result.RolledBack)
if assert.Len(t, result.Applied, 1) {
if assert.NotNil(t, result.Applied[0].PreviousValue) {
assert.Equal(t, int16(4), *result.Applied[0].PreviousValue)
}
}
}
func Test_mk2Ser_DriverDiagnostics(t *testing.T) {
m := &mk2Ser{
traceLimit: 10,
traces: make([]ProtocolTrace, 0, 10),
}
m.appendTrace(ProtocolTrace{
Timestamp: time.Now().UTC(),
Direction: TraceDirectionTX,
Frame: "0x57",
Command: "winmon:0x30",
BytesHex: "AA",
})
m.noteCommandFailure(assert.AnError)
m.noteCommandTimeout(assert.AnError)
m.checksumErrors.Add(1)
m.markFrameSeen()
diag := m.DriverDiagnostics(10)
assert.NotEmpty(t, diag.Traces)
assert.Greater(t, diag.CommandFailures, uint64(0))
assert.Greater(t, diag.CommandTimeouts, uint64(0))
assert.Greater(t, diag.ChecksumFailures, uint64(0))
assert.GreaterOrEqual(t, diag.HealthScore, 0)
}
func Test_parseFrameLength(t *testing.T) {
tests := []struct {
name string
raw byte
expectedLen byte
expectedFlag bool
}{
{
name: "normal length",
raw: 0x07,
expectedLen: 0x07,
expectedFlag: false,
},
{
name: "length with LED flag",
raw: 0x86,
expectedLen: 0x06,
expectedFlag: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
length, hasLEDStatus := parseFrameLength(tt.raw)
assert.Equal(t, tt.expectedLen, length)
assert.Equal(t, tt.expectedFlag, hasLEDStatus)
})
}
}
func Test_mk2Ser_handleFrame_StateFrameWithAppendedLED(t *testing.T) {
m := &mk2Ser{
info: &Mk2Info{},
stateAck: make(chan struct{}, 1),
}
length, frame := buildTestFrame(frameHeader, stateFrame)
m.handleFrame(length, frame, []byte{0x03, 0x00})
select {
case <-m.stateAck:
default:
t.Fatal("expected state acknowledgement")
}
assert.NotNil(t, m.info.LEDs)
assert.Equal(t, LedOn, m.info.LEDs[LedMain])
assert.Equal(t, LedOn, m.info.LEDs[LedAbsorption])
assert.Equal(t, LedOff, m.info.LEDs[LedBulk])
}