Repository: evcc-io/evcc
Pull request #6197 (2023-02-11 12:07:57)

Sessions: use meter energy reading to correct session charged energy
Fix https://github.com/evcc-io/evcc/issues/5696


Patch:
diff --git a/core/db/db.go b/core/db/db.go
index 8d38ebee..692ae001 100644
--- a/core/db/db.go
+++ b/core/db/db.go
@@ -20,13 +20,24 @@ type Database interface {
 
 // New creates a database storage driver
 func New(name string) (*DB, error) {
-	db := &DB{
+	db := serverdb.Instance
+
+	// TODO deprecate
+	var err error
+	if table := "transactions"; db.Migrator().HasTable(table) {
+		err = db.Migrator().RenameTable(table, new(Session))
+	}
+	if err == nil {
+		err = db.AutoMigrate(new(Session))
+	}
+
+	sessiondb := &DB{
 		log:  util.NewLogger("db"),
-		db:   serverdb.Instance,
+		db:   db,
 		name: name,
 	}
 
-	return db, nil
+	return sessiondb, err
 }
 
 // Session creates a charging session
@@ -45,3 +56,10 @@ func (s *DB) Persist(session interface{}) {
 		s.log.ERROR.Printf("persist: %v", err)
 	}
 }
+
+// Return sessions
+func (s *DB) Sessions() (Sessions, error) {
+	var res Sessions
+	tx := s.db.Find(&res)
+	return res, tx.Error
+}
diff --git a/core/loadpoint_session.go b/core/loadpoint_session.go
index 91f52257..b5a054d8 100644
--- a/core/loadpoint_session.go
+++ b/core/loadpoint_session.go
@@ -1,8 +1,6 @@
 package core
 
 import (
-	"time"
-
 	"github.com/evcc-io/evcc/api"
 	"github.com/evcc-io/evcc/core/db"
 )
@@ -55,10 +53,15 @@ func (lp *Loadpoint) stopSession() {
 		return
 	}
 
-	lp.session.Finished = time.Now()
+	lp.session.Finished = lp.clock.Now()
 	lp.session.MeterStop = lp.chargeMeterTotal()
 
-	if chargedEnergy := lp.getChargedEnergy() / 1e3; chargedEnergy > lp.session.ChargedEnergy {
+	chargedEnergy := lp.getChargedEnergy() / 1e3
+	if delta := lp.session.MeterStop - lp.session.MeterStart; delta < chargedEnergy && lp.session.MeterStart*lp.session.MeterStop > 0 {
+		chargedEnergy = delta
+	}
+
+	if chargedEnergy > lp.session.ChargedEnergy {
 		lp.session.ChargedEnergy = chargedEnergy
 	}
 
diff --git a/core/site.go b/core/site.go
index b8f6262b..95ba444a 100644
--- a/core/site.go
+++ b/core/site.go
@@ -110,21 +110,6 @@ func NewSiteFromConfig(
 	site.coordinator = coordinator.New(log, vehicles)
 	site.savings = NewSavings(tariffs)
 
-	// migrate session log
-	if serverdb.Instance != nil {
-		var err error
-		// TODO deprecate
-		if table := "transactions"; serverdb.Instance.Migrator().HasTable(table) {
-			err = serverdb.Instance.Migrator().RenameTable(table, new(db.Session))
-		}
-		if err == nil {
-			err = serverdb.Instance.AutoMigrate(new(db.Session))
-		}
-		if err != nil {
-			return nil, err
-		}
-	}
-
 	// upload telemetry on shutdown
 	if telemetry.Enabled() {
 		shutdown.Register(func() {


Test patch:
diff --git a/core/loadpoint_session_test.go b/core/loadpoint_session_test.go
new file mode 100644
index 00000000..95d62494
--- /dev/null
+++ b/core/loadpoint_session_test.go
@@ -0,0 +1,87 @@
+package core
+
+import (
+	"testing"
+	"time"
+
+	"github.com/benbjohnson/clock"
+	"github.com/evcc-io/evcc/api"
+	coredb "github.com/evcc-io/evcc/core/db"
+	"github.com/evcc-io/evcc/mock"
+	serverdb "github.com/evcc-io/evcc/server/db"
+	"github.com/golang/mock/gomock"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestSession(t *testing.T) {
+	var err error
+	serverdb.Instance, err = serverdb.New("sqlite", ":memory:")
+	assert.NoError(t, err)
+
+	db, err := coredb.New("foo")
+	assert.NoError(t, err)
+
+	clock := clock.NewMock()
+
+	ctrl := gomock.NewController(t)
+	defer ctrl.Finish()
+
+	mm := mock.NewMockMeter(ctrl)
+	me := mock.NewMockMeterEnergy(ctrl)
+
+	type EnergyDecorator struct {
+		api.Meter
+		api.MeterEnergy
+	}
+
+	cm := &EnergyDecorator{Meter: mm, MeterEnergy: me}
+
+	lp := &Loadpoint{
+		clock:       clock,
+		db:          db,
+		chargeMeter: cm,
+	}
+
+	// create session
+	me.EXPECT().TotalEnergy().Return(1.0, nil)
+	lp.createSession()
+	assert.NotNil(t, lp.session)
+
+	// start charging
+	lp.updateSession(func(session *coredb.Session) {
+		if session.Created.IsZero() {
+			session.Created = lp.clock.Now()
+		}
+	})
+	assert.Equal(t, clock.Now(), lp.session.Created)
+
+	// stop charging
+	clock.Add(time.Hour)
+	lp.chargedEnergy = 1.23 * 1e3                                   // Wh
+	me.EXPECT().TotalEnergy().Return(1.0+lp.chargedEnergy/1e3, nil) // match chargedEnergy
+
+	lp.stopSession()
+	assert.NotNil(t, lp.session)
+	assert.Equal(t, lp.chargedEnergy/1e3, lp.session.ChargedEnergy)
+	assert.Equal(t, clock.Now(), lp.session.Finished)
+
+	s, err := db.Sessions()
+	assert.NoError(t, err)
+	assert.Len(t, s, 1)
+	t.Log(s)
+
+	// stop charging - 2nd leg
+	clock.Add(time.Hour)
+	lp.chargedEnergy *= 2
+	me.EXPECT().TotalEnergy().Return(3.0, nil) // doesn't match chargedEnergy
+
+	lp.stopSession()
+	assert.NotNil(t, lp.session)
+	assert.Equal(t, 3.0-1.0, lp.session.ChargedEnergy) // expect actual meter energy delta
+	assert.Equal(t, clock.Now(), lp.session.Finished)
+
+	s, err = db.Sessions()
+	assert.NoError(t, err)
+	assert.Len(t, s, 1)
+	t.Log(s)
+}