Repository: andig/evcc
Pull request #253 (2020-07-29 20:32:51)

Always disable charger when car disconnected to avoid unnecessary swi…
…tching activity


Patch:
diff --git a/.goreleaser.yml b/.goreleaser.yml
index 3ecf65fa..f15bb0f9 100644
--- a/.goreleaser.yml
+++ b/.goreleaser.yml
@@ -8,6 +8,8 @@ before:
 builds:
 - id: evcc
   main: main.go
+  flags:
+  - -tags=release
   ldflags:
   - -X github.com/andig/evcc/server.Version={{.Version}} -X github.com/andig/evcc/server.Commit={{.ShortCommit}}
   env:
diff --git a/Makefile b/Makefile
index 5e6f82f2..532330dd 100644
--- a/Makefile
+++ b/Makefile
@@ -1,6 +1,6 @@
 .PHONY: default clean install lint test assets build binaries publish-images test-release release
 
-TAG_NAME := $(shell git describe --abbrev=0 --tags).0
+TAG_NAME := $(shell git describe --abbrev=0 --tags)
 SHA := $(shell git rev-parse --short HEAD)
 VERSION := $(if $(TAG_NAME),$(TAG_NAME),$(SHA))
 
@@ -29,7 +29,7 @@ assets:
 
 build:
 	@echo Version: $(VERSION) $(BUILD_DATE)
-	go build -v -tags release -ldflags '-X "github.com/andig/evcc/server.Version=${VERSION}" -X "github.com/andig/evcc/server.Commit=${SHA}"'
+	go build -v -tags=release -ldflags '-X "github.com/andig/evcc/server.Version=${VERSION}" -X "github.com/andig/evcc/server.Commit=${SHA}"'
 
 publish-images:
 	@echo Version: $(VERSION) $(BUILD_DATE)
diff --git a/api/api.go b/api/api.go
index c9c57897..d17ecb54 100644
--- a/api/api.go
+++ b/api/api.go
@@ -2,7 +2,7 @@ package api
 
 import "time"
 
-//go:generate mockgen -package mock -destination ../mock/mock_api.go github.com/andig/evcc/api Charger,Meter,MeterEnergy
+//go:generate mockgen -package mock -destination ../mock/mock_api.go github.com/andig/evcc/api Charger,Meter,MeterEnergy,Vehicle
 
 // ChargeMode are charge modes modeled after OpenWB
 type ChargeMode string
diff --git a/core/loadpoint.go b/core/loadpoint.go
index 05c82b86..dc13b46c 100644
--- a/core/loadpoint.go
+++ b/core/loadpoint.go
@@ -542,8 +542,15 @@ func (lp *LoadPoint) remainingChargeDuration(chargePercent float64) time.Duratio
 	}
 
 	if lp.chargePower > 0 && lp.vehicle != nil {
-		whTotal := int64(lp.TargetSoC) * lp.vehicle.Capacity()
-		whRemaining := (1 - chargePercent/100.0) * 1e3 * float64(whTotal)
+		chargePercent = chargePercent / 100.0
+		targetPercent := float64(lp.TargetSoC) / 100
+
+		if chargePercent >= targetPercent {
+			return 0
+		}
+
+		whTotal := float64(lp.vehicle.Capacity()) * 1e3
+		whRemaining := (targetPercent - chargePercent) * whTotal
 		return time.Duration(float64(time.Hour) * whRemaining / lp.chargePower).Round(time.Second)
 	}
 
@@ -612,6 +619,11 @@ func (lp *LoadPoint) Update(sitePower float64) {
 
 	// execute loading strategy
 	switch {
+	case !lp.connected():
+		// always disable charger if not connected
+		// https://github.com/andig/evcc/issues/105
+		err = lp.handler.Ramp(0)
+
 	case lp.TargetSoC > 0 && lp.vehicle != nil && lp.socCharge >= float64(lp.TargetSoC):
 		err = lp.handler.Ramp(0)
 
@@ -619,20 +631,10 @@ func (lp *LoadPoint) Update(sitePower float64) {
 		err = lp.handler.Ramp(0, true)
 
 	case mode == api.ModeNow:
-		// ensure that new connections happen at min current
-		current := lp.MinCurrent
-		if lp.connected() {
-			current = lp.MaxCurrent
-		}
-		err = lp.handler.Ramp(current, true)
+		err = lp.handler.Ramp(lp.MaxCurrent, true)
 
 	case mode == api.ModeMinPV || mode == api.ModePV:
 		targetCurrent := lp.maxCurrent(mode, sitePower)
-		if !lp.connected() {
-			// ensure minimum current when not connected
-			// https://github.com/andig/evcc/issues/105
-			targetCurrent = min(lp.MinCurrent, targetCurrent)
-		}
 		lp.log.DEBUG.Printf("target charge current: %dA", targetCurrent)
 
 		err = lp.handler.Ramp(targetCurrent)
diff --git a/mock/mock_api.go b/mock/mock_api.go
index 468528ce..834757a2 100644
--- a/mock/mock_api.go
+++ b/mock/mock_api.go
@@ -1,5 +1,5 @@
 // Code generated by MockGen. DO NOT EDIT.
-// Source: github.com/andig/evcc/api (interfaces: Charger,Meter,MeterEnergy)
+// Source: github.com/andig/evcc/api (interfaces: Charger,Meter,MeterEnergy,Vehicle)
 
 // Package mock is a generated GoMock package.
 package mock
@@ -166,3 +166,69 @@ func (mr *MockMeterEnergyMockRecorder) TotalEnergy() *gomock.Call {
 	mr.mock.ctrl.T.Helper()
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TotalEnergy", reflect.TypeOf((*MockMeterEnergy)(nil).TotalEnergy))
 }
+
+// MockVehicle is a mock of Vehicle interface
+type MockVehicle struct {
+	ctrl     *gomock.Controller
+	recorder *MockVehicleMockRecorder
+}
+
+// MockVehicleMockRecorder is the mock recorder for MockVehicle
+type MockVehicleMockRecorder struct {
+	mock *MockVehicle
+}
+
+// NewMockVehicle creates a new mock instance
+func NewMockVehicle(ctrl *gomock.Controller) *MockVehicle {
+	mock := &MockVehicle{ctrl: ctrl}
+	mock.recorder = &MockVehicleMockRecorder{mock}
+	return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use
+func (m *MockVehicle) EXPECT() *MockVehicleMockRecorder {
+	return m.recorder
+}
+
+// Capacity mocks base method
+func (m *MockVehicle) Capacity() int64 {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "Capacity")
+	ret0, _ := ret[0].(int64)
+	return ret0
+}
+
+// Capacity indicates an expected call of Capacity
+func (mr *MockVehicleMockRecorder) Capacity() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Capacity", reflect.TypeOf((*MockVehicle)(nil).Capacity))
+}
+
+// ChargeState mocks base method
+func (m *MockVehicle) ChargeState() (float64, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "ChargeState")
+	ret0, _ := ret[0].(float64)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// ChargeState indicates an expected call of ChargeState
+func (mr *MockVehicleMockRecorder) ChargeState() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ChargeState", reflect.TypeOf((*MockVehicle)(nil).ChargeState))
+}
+
+// Title mocks base method
+func (m *MockVehicle) Title() string {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "Title")
+	ret0, _ := ret[0].(string)
+	return ret0
+}
+
+// Title indicates an expected call of Title
+func (mr *MockVehicleMockRecorder) Title() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Title", reflect.TypeOf((*MockVehicle)(nil).Title))
+}


Test patch:
diff --git a/core/loadpoint_test.go b/core/loadpoint_test.go
index e87520e8..979d4bc9 100644
--- a/core/loadpoint_test.go
+++ b/core/loadpoint_test.go
@@ -79,13 +79,13 @@ func TestUpdate(t *testing.T) {
 		expect func(h *mock.MockHandler)
 	}{
 		{api.StatusA, api.ModeOff, func(h *mock.MockHandler) {
-			h.EXPECT().Ramp(int64(0), true)
+			h.EXPECT().Ramp(int64(0))
 		}},
 		{api.StatusA, api.ModeNow, func(h *mock.MockHandler) {
-			h.EXPECT().Ramp(lpMinCurrent, true)
+			h.EXPECT().Ramp(int64(0))
 		}},
 		{api.StatusA, api.ModeMinPV, func(h *mock.MockHandler) {
-			h.EXPECT().Ramp(lpMinCurrent)
+			h.EXPECT().Ramp(int64(0))
 		}},
 		{api.StatusA, api.ModePV, func(h *mock.MockHandler) {
 			h.EXPECT().Ramp(int64(0)) // zero since update called with 0
@@ -150,10 +150,10 @@ func TestUpdate(t *testing.T) {
 
 		if tc.status != api.StatusA {
 			handler.EXPECT().SyncEnabled()
-		}
 
-		if tc.mode == api.ModeMinPV || tc.mode == api.ModePV {
-			handler.EXPECT().TargetCurrent().Return(int64(0))
+			if tc.mode == api.ModeMinPV || tc.mode == api.ModePV {
+				handler.EXPECT().TargetCurrent().Return(int64(0))
+			}
 		}
 
 		if tc.expect != nil {
@@ -167,33 +167,6 @@ func TestUpdate(t *testing.T) {
 	}
 }
 
-// func TestConsumedPower(t *testing.T) {
-// 	tc := []struct {
-// 		grid, pv, battery, consumed float64
-// 	}{
-// 		{0, 0, 0, 0},    // silent night
-// 		{1, 0, 0, 1},    // grid import
-// 		{0, 1, 0, 1},    // pv sign ignored
-// 		{0, -1, 0, 1},   // pv sign ignored
-// 		{1, 1, 0, 2},    // grid import + pv, pv sign ignored
-// 		{1, -1, 0, 2},   // grid import + pv, pv sign ignored
-// 		{0, 0, 1, 1},    // battery discharging
-// 		{0, 0, -1, -1},  // battery charging -> negative result cannot occur in reality
-// 		{1, -3, 1, 5},   // grid import + pv + battery discharging
-// 		{1, -3, -1, 3},  // grid import + pv + battery charging -> should not happen in reality
-// 		{0, -3, -1, 2},  // pv + battery charging
-// 		{-1, -4, -1, 2}, // grid export + pv + battery charging
-// 		{-1, -4, 0, 3},  // grid export + pv
-// 	}
-
-// 	for _, tc := range tc {
-// 		res := consumedPower(tc.pv, tc.battery, tc.grid)
-// 		if res != tc.consumed {
-// 			t.Errorf("consumedPower wanted %.f, got %.f", tc.consumed, res)
-// 		}
-// 	}
-// }
-
 func TestPVHysteresisForStatusC(t *testing.T) {
 	dt := time.Minute
 	type se struct {
@@ -380,3 +353,23 @@ func TestPVHysteresisForStatusOtherThanC(t *testing.T) {
 
 	ctrl.Finish()
 }
+
+func TestRemainingChargeDuration(t *testing.T) {
+	lp := NewLoadPoint(util.NewLogger("foo"))
+
+	ctrl := gomock.NewController(t)
+	vehicle := mock.NewMockVehicle(ctrl)
+
+	lp.vehicle = vehicle
+	lp.charging = true
+
+	soc := 20.0
+	lp.TargetSoC = 80
+	lp.chargePower = 1000
+
+	vehicle.EXPECT().Capacity().Return(int64(10))
+
+	if remaining := lp.remainingChargeDuration(soc); remaining != 6*time.Hour {
+		t.Error("wrong remaining charge duration")
+	}
+}