package core import ( "testing" "time" "github.com/evcc-io/evcc/api" "github.com/evcc-io/evcc/util" "github.com/evcc-io/evcc/util/config" "github.com/stretchr/testify/assert" "go.uber.org/mock/gomock" ) func TestApplyBatteryMode(t *testing.T) { for _, tc := range []struct { internal, expected api.BatteryMode }{ {api.BatteryUnknown, api.BatteryUnknown}, // no change required {api.BatteryNormal, api.BatteryUnknown}, // no change required {api.BatteryHold, api.BatteryNormal}, {api.BatteryCharge, api.BatteryNormal}, } { t.Logf("%+v", tc) ctrl := gomock.NewController(t) var bat api.Meter batCon := api.NewMockBatteryController(ctrl) bat = &struct { api.Meter api.BatteryController }{ BatteryController: batCon, } site := &Site{ log: util.NewLogger("foo"), batteryMeters: []config.Device[api.Meter]{config.NewStaticDevice(config.Named{}, bat)}, batteryMode: tc.internal, } // verify mode applied to battery if tc.expected != api.BatteryUnknown { batCon.EXPECT().SetBatteryMode(tc.expected).Times(1) } site.updateBatteryMode(false, api.Rate{}) if tc.internal != api.BatteryNormal { assert.Equal(t, tc.expected, site.batteryMode) } ctrl.Finish() } } func TestRequiredExternalBatteryMode(t *testing.T) { for _, tc := range []struct { internal, external, new api.BatteryMode }{ {api.BatteryUnknown, api.BatteryUnknown, api.BatteryUnknown}, {api.BatteryUnknown, api.BatteryNormal, api.BatteryNormal}, {api.BatteryUnknown, api.BatteryCharge, api.BatteryCharge}, {api.BatteryNormal, api.BatteryUnknown, api.BatteryUnknown}, {api.BatteryNormal, api.BatteryNormal, api.BatteryUnknown}, // no change required {api.BatteryNormal, api.BatteryCharge, api.BatteryCharge}, {api.BatteryCharge, api.BatteryUnknown, api.BatteryNormal}, {api.BatteryCharge, api.BatteryNormal, api.BatteryNormal}, {api.BatteryCharge, api.BatteryCharge, api.BatteryUnknown}, // no change required } { t.Logf("%+v", tc) site := &Site{ log: util.NewLogger("foo"), batteryMeters: []config.Device[api.Meter]{nil}, } site.batteryMode = tc.internal site.batteryModeExternal = tc.external mode := site.requiredBatteryMode(false, api.Rate{}) assert.Equal(t, tc.new.String(), mode.String(), "internal mode expected %s got %s", tc.new, mode) } } func TestExternalBatteryModeChange(t *testing.T) { for _, tc := range []struct { internal, external, expected api.BatteryMode }{ {api.BatteryUnknown, api.BatteryUnknown, api.BatteryUnknown}, {api.BatteryUnknown, api.BatteryNormal, api.BatteryNormal}, {api.BatteryUnknown, api.BatteryCharge, api.BatteryCharge}, {api.BatteryNormal, api.BatteryUnknown, api.BatteryUnknown}, {api.BatteryNormal, api.BatteryNormal, api.BatteryUnknown}, {api.BatteryNormal, api.BatteryCharge, api.BatteryCharge}, {api.BatteryHold, api.BatteryUnknown, api.BatteryNormal}, // return to normal {api.BatteryHold, api.BatteryNormal, api.BatteryNormal}, {api.BatteryHold, api.BatteryHold, api.BatteryUnknown}, {api.BatteryCharge, api.BatteryUnknown, api.BatteryNormal}, // return to normal {api.BatteryCharge, api.BatteryNormal, api.BatteryNormal}, {api.BatteryCharge, api.BatteryCharge, api.BatteryUnknown}, } { t.Logf("%+v", tc) ctrl := gomock.NewController(t) var bat api.Meter batCon := api.NewMockBatteryController(ctrl) bat = &struct { api.Meter api.BatteryController }{ BatteryController: batCon, } site := &Site{ log: util.NewLogger("foo"), batteryMeters: []config.Device[api.Meter]{config.NewStaticDevice(config.Named{}, bat)}, batteryMode: tc.internal, } // 1. set required external mode site.SetBatteryModeExternal(tc.external) assert.Equal(t, site.batteryModeExternal, tc.external, "external mode expected %s got %s", tc.external, site.batteryModeExternal) assert.Equal(t, site.batteryMode, tc.internal, "internal mode expected unchanged %s got %s", tc.internal, site.batteryMode) // 2. verify external mode applied to battery if tc.expected != api.BatteryUnknown { batCon.EXPECT().SetBatteryMode(tc.expected).Times(1) } site.updateBatteryMode(false, api.Rate{}) if !ctrl.Satisfied() { ctrl.Finish() } // 3. verify required external mode only applied once site.updateBatteryMode(false, api.Rate{}) if !ctrl.Satisfied() { ctrl.Finish() } // 4. verify timer expiry site.batteryModeExternalTimer = site.batteryModeExternalTimer.Add(-time.Hour) site.batteryModeWatchdogExpired() // mode reverted to unknown, timer still active assert.Equal(t, site.batteryModeExternal, api.BatteryUnknown) assert.False(t, site.batteryModeExternalTimer.IsZero()) // battery switched back to normal mode batCon.EXPECT().SetBatteryMode(api.BatteryNormal).Times(1) site.updateBatteryMode(false, api.Rate{}) // timer disabled assert.True(t, site.batteryModeExternalTimer.IsZero()) ctrl.Finish() } } func TestForcedBatteryChargeLimits(t *testing.T) { limit := 80.0 for _, tc := range []struct { internal, expected api.BatteryMode soc float64 }{ {api.BatteryUnknown, api.BatteryCharge, 50}, {api.BatteryUnknown, api.BatteryHold, 90}, {api.BatteryNormal, api.BatteryCharge, 50}, {api.BatteryNormal, api.BatteryHold, 90}, {api.BatteryHold, api.BatteryCharge, 50}, {api.BatteryHold, api.BatteryHold, 90}, // TODO make this api.BatteryUnknown {api.BatteryCharge, api.BatteryUnknown, 50}, {api.BatteryCharge, api.BatteryHold, 90}, } { t.Logf("%+v", tc) ctrl := gomock.NewController(t) var bat api.Meter batSoc := api.NewMockBattery(ctrl) batCon := api.NewMockBatteryController(ctrl) batSocLimit := api.NewMockBatterySocLimiter(ctrl) bat = &struct { api.Meter api.Battery api.BatteryController api.BatterySocLimiter }{ Meter: bat, Battery: batSoc, BatteryController: batCon, BatterySocLimiter: batSocLimit, } site := &Site{ log: util.NewLogger("foo"), batteryMeters: []config.Device[api.Meter]{config.NewStaticDevice(config.Named{}, bat)}, batteryMode: tc.internal, } batSoc.EXPECT().Soc().Return(tc.soc, nil).Times(1) batSocLimit.EXPECT().GetSocLimits().Return(0.0, limit).Times(1) if tc.expected != api.BatteryUnknown { batCon.EXPECT().SetBatteryMode(tc.expected).Times(1) } site.updateBatteryMode(true, api.Rate{}) ctrl.Finish() } }