package ankersolix import ( "context" "encoding/binary" "strings" "testing" ) // liveBlock builds a 20041-20100 register block with every register zero, ready // for a test to set the ones it cares about by address. type liveBlock []uint16 func newLiveBlock() liveBlock { return make(liveBlock, liveCount) } func (b liveBlock) set(addr int, v uint16) { b[addr-liveStart] = v } func (b liveBlock) set32(addr int, v uint32) { b[addr-liveStart] = uint16(v >> 16) b[addr-liveStart+1] = uint16(v) } func TestDecodeLiveScalesMeasurements(t *testing.T) { b := newLiveBlock() b.set(regVoltageL1N, 2301) // 230.1 V, gain 10 b.set(regVoltageL1N+1, 2312) // 231.2 V b.set(regVoltageL1N+2, 2298) // 229.8 V b.set(regCurrentL1, 1600) // 16.00 A, gain 100 b.set(regCurrentL1+1, 1598) // 15.98 A b.set(regCurrentL1+2, 0) b.set32(regPowerL1, 3680) b.set32(regPowerTotal, 11040) b.set32(regSessionSec, 3725) b.set32(regSessionWh, 12500) var snap ModbusSnapshot decodeLive(&snap, b) if snap.VoltageL1 == nil || *snap.VoltageL1 != 230.1 { t.Errorf("VoltageL1 = %v, want 230.1", snap.VoltageL1) } if snap.VoltageL3 == nil || *snap.VoltageL3 != 229.8 { t.Errorf("VoltageL3 = %v, want 229.8", snap.VoltageL3) } if snap.CurrentL1 == nil || *snap.CurrentL1 != 16.0 { t.Errorf("CurrentL1 = %v, want 16.0", snap.CurrentL1) } if snap.CurrentL2 == nil || *snap.CurrentL2 != 15.98 { t.Errorf("CurrentL2 = %v, want 15.98", snap.CurrentL2) } // The 32-bit values straddle two registers; a swapped pair would read as a // wildly different number rather than a near miss. if snap.PowerL1 == nil || *snap.PowerL1 != 3680 { t.Errorf("PowerL1 = %v, want 3680", snap.PowerL1) } if snap.PowerTotal == nil || *snap.PowerTotal != 11040 { t.Errorf("PowerTotal = %v, want 11040", snap.PowerTotal) } if snap.SessionSeconds == nil || *snap.SessionSeconds != 3725 { t.Errorf("SessionSeconds = %v, want 3725", snap.SessionSeconds) } if snap.SessionWh == nil || *snap.SessionWh != 12500 { t.Errorf("SessionWh = %v, want 12500", snap.SessionWh) } } func TestDecodeLiveNamesStates(t *testing.T) { b := newLiveBlock() b.set(regChargingStatus, 2) // charging b.set(regCPSignal, 5) // C1, vehicle drawing b.set(regOCPPStatus, 2) // connected b.set(regMQTTStatus, 1) // connected b.set(regBoostMode, 1) b.set(regLoadBalancing, 0) b.set(regLEDBrightness, 70) var snap ModbusSnapshot decodeLive(&snap, b) // The Modbus status enum is the same one the cloud reports, so it must decode // to the same names the rest of the plugin already uses. if snap.Status == nil || *snap.Status != 2 || snap.StatusDesc != stateCharging { t.Errorf("status = %v/%q, want 2/%q", snap.Status, snap.StatusDesc, stateCharging) } if snap.CPSignalDesc != cpSignalNames[5] { t.Errorf("CPSignalDesc = %q, want %q", snap.CPSignalDesc, cpSignalNames[5]) } if snap.OcppStatusDesc != "connected" { t.Errorf("OcppStatusDesc = %q, want connected", snap.OcppStatusDesc) } // Register value 1 means "connected" for MQTT but only "connecting" for OCPP; // the two registers must not be decoded through one table. if snap.MqttStatusDesc != "connected" { t.Errorf("MqttStatusDesc = %q, want connected", snap.MqttStatusDesc) } if snap.BoostMode == nil || !*snap.BoostMode { t.Errorf("BoostMode = %v, want true", snap.BoostMode) } if snap.LoadBalancing == nil || *snap.LoadBalancing { t.Errorf("LoadBalancing = %v, want false", snap.LoadBalancing) } if snap.LEDBrightness == nil || *snap.LEDBrightness != 70 { t.Errorf("LEDBrightness = %v, want 70", snap.LEDBrightness) } } func TestDecodeLiveReadsNegativeTemperatures(t *testing.T) { b := newLiveBlock() below := int16(-7) b.set(regRelay1Temp, uint16(below)) b.set(regRelay2Temp, 41) var snap ModbusSnapshot decodeLive(&snap, b) if snap.Relay1TempC == nil || *snap.Relay1TempC != -7 { t.Errorf("Relay1TempC = %v, want -7", snap.Relay1TempC) } if snap.Relay2TempC == nil || *snap.Relay2TempC != 41 { t.Errorf("Relay2TempC = %v, want 41", snap.Relay2TempC) } } func TestDecodeLiveFlagsAlarms(t *testing.T) { quiet := newLiveBlock() var snap ModbusSnapshot decodeLive(&snap, quiet) if snap.Alarm { t.Error("Alarm set with every alarm word zero") } if len(snap.Alarms) != 12 { t.Errorf("got %d alarm words, want 12", len(snap.Alarms)) } noisy := newLiveBlock() noisy.set(regAlarm1+7, 0x0040) var snap2 ModbusSnapshot decodeLive(&snap2, noisy) if !snap2.Alarm { t.Error("Alarm not set although an alarm bit is on") } if snap2.Alarms[7] != 0x0040 { t.Errorf("alarm word 8 = 0x%04x, want 0x0040", snap2.Alarms[7]) } } // A short block must leave fields absent rather than decode whatever follows. func TestDecodeLiveToleratesShortBlock(t *testing.T) { var snap ModbusSnapshot decodeLive(&snap, []uint16{1, 2, 3}) if snap.Status != nil { t.Errorf("Status = %v, want nil from a truncated block", snap.Status) } if snap.PowerTotal != nil { t.Errorf("PowerTotal = %v, want nil from a truncated block", snap.PowerTotal) } } func TestDecodeIdentityReadsStrings(t *testing.T) { regs := make([]uint16, identityCount) put := func(addr int, s string, count int) { b := []byte(s) for len(b) < count*2 { b = append(b, 0) } for i := 0; i < count; i++ { regs[addr-identityStart+i] = binary.BigEndian.Uint16(b[i*2:]) } } put(regModelName, "A5191", 10) put(regSerialNumber, "V1SN0123456789AB", 12) put(regSoftwareVersion, "1.2.3", 6) put(regHardwareVersion, "1.0", 6) var snap ModbusSnapshot decodeIdentity(&snap, regs) if snap.Model != "A5191" { t.Errorf("Model = %q, want A5191", snap.Model) } if snap.Serial != "V1SN0123456789AB" { t.Errorf("Serial = %q, want V1SN0123456789AB", snap.Serial) } if snap.Firmware != "1.2.3" { t.Errorf("Firmware = %q, want 1.2.3", snap.Firmware) } if snap.Hardware != "1.0" { t.Errorf("Hardware = %q, want 1.0", snap.Hardware) } } // Below 6 A the charger stops instead of charging slowly, so a limit in that // range has to be refused rather than quietly turned into a pause. func TestModbusSetMaxCurrentRefusesBelowFloor(t *testing.T) { err := ModbusSetMaxCurrent(context.Background(), nil, 4) if err == nil { t.Fatal("4 A was accepted") } if !strings.Contains(err.Error(), "pauses charging") { t.Errorf("error = %q, want it to explain the pause", err) } } func TestModbusSetMaxCurrentRefusesOutOfRange(t *testing.T) { for _, amps := range []float64{-1, 40} { if err := ModbusSetMaxCurrent(context.Background(), nil, amps); err == nil { t.Errorf("%.0f A was accepted", amps) } } } func TestModbusSetPhaseModeRejectsUnknownMode(t *testing.T) { if err := ModbusSetPhaseMode(context.Background(), nil, 3); err == nil { t.Fatal("phase mode 3 was accepted") } } func TestModbusSetTimeoutEnforcesFloor(t *testing.T) { if err := ModbusSetTimeout(context.Background(), nil, 5); err == nil { t.Fatal("a 5 second timeout was accepted, but the spec requires more than 5") } } func TestModbusConfigDefaultsToPort502(t *testing.T) { if got := (ModbusConfig{Host: "10.2.1.55"}).Address(); got != "10.2.1.55:502" { t.Errorf("address = %q, want 10.2.1.55:502", got) } if got := (ModbusConfig{Host: "10.2.1.55", Port: 1502}).Address(); got != "10.2.1.55:1502" { t.Errorf("address = %q, want 10.2.1.55:1502", got) } } func TestModbusDialRequiresHost(t *testing.T) { if _, err := ModbusDial(context.Background(), ModbusConfig{}); err == nil { t.Fatal("an empty host was accepted") } }