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, } } // 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(commandWriteSettingResponse) }() err := m.WriteSetting(0x1234, 1234) assert.NoError(t, err) expected := []byte{ 0x05, 0xff, 0x57, 0x33, 0x34, 0x12, 0x2c, 0x05, 0xff, 0x57, 0x34, 0xd2, 0x04, 0x9b, } 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, ¤tLimit) 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()) }