Repository: evcc-io/evcc
Pull request #10313 (2023-10-14 11:35:35)

chore: align implementation of phase-value returns
Benefits:

- simple copy/paste of code
- less allocations (not that performance matter..)

open: single-request phase reading for Wallbe


Patch:
diff --git a/charger/abb.go b/charger/abb.go
index b2ff703f..8fa80baa 100644
--- a/charger/abb.go
+++ b/charger/abb.go
@@ -218,12 +218,12 @@ func (wb *ABB) Currents() (float64, float64, float64, error) {
 		return 0, 0, 0, err
 	}
 
-	var curr [3]float64
+	var res [3]float64
 	for l := 0; l < 3; l++ {
-		curr[l] = float64(binary.BigEndian.Uint32(b[4*l:])) / 1e3
+		res[l] = float64(binary.BigEndian.Uint32(b[4*l:])) / 1e3
 	}
 
-	return curr[0], curr[1], curr[2], nil
+	return res[0], res[1], res[2], nil
 }
 
 var _ api.PhaseVoltages = (*ABB)(nil)
@@ -235,12 +235,12 @@ func (wb *ABB) Voltages() (float64, float64, float64, error) {
 		return 0, 0, 0, err
 	}
 
-	var volt [3]float64
+	var res [3]float64
 	for l := 0; l < 3; l++ {
-		volt[l] = float64(binary.BigEndian.Uint32(b[4*l:])) / 10
+		res[l] = float64(binary.BigEndian.Uint32(b[4*l:])) / 10
 	}
 
-	return volt[0], volt[1], volt[2], nil
+	return res[0], res[1], res[2], nil
 }
 
 // var _ api.PhaseSwitcher = (*ABB)(nil)
diff --git a/charger/abl.go b/charger/abl.go
index 84012f2c..ea7b70ca 100644
--- a/charger/abl.go
+++ b/charger/abl.go
@@ -236,17 +236,17 @@ func (wb *ABLeMH) currents() (float64, float64, float64, error) {
 		return 0, 0, 0, err
 	}
 
-	var currents []float64
-	for i := 2; i < 5; i++ {
-		u := binary.BigEndian.Uint16(b[2*i:])
+	var res [3]float64
+	for i := 0; i < 3; i++ {
+		u := binary.BigEndian.Uint16(b[2*(2+i):])
 		if u == ablAmpsDisabled || u == 1 {
 			u = 0
 		}
 
-		currents = append(currents, float64(u)/10)
+		res[i] = float64(u) / 10
 	}
 
-	return currents[2], currents[1], currents[0], nil
+	return res[2], res[1], res[0], nil
 }
 
 var _ api.Diagnosis = (*ABLeMH)(nil)
diff --git a/charger/bender.go b/charger/bender.go
index 45ea2d40..057ca532 100644
--- a/charger/bender.go
+++ b/charger/bender.go
@@ -287,12 +287,12 @@ func (wb *BenderCC) currents() (float64, float64, float64, error) {
 		return 0, 0, 0, err
 	}
 
-	var curr [3]float64
+	var res [3]float64
 	for l := 0; l < 3; l++ {
-		curr[l] = float64(binary.BigEndian.Uint32(b[4*l:4*(l+1)])) / 1e3
+		res[l] = float64(binary.BigEndian.Uint32(b[4*l:4*(l+1)])) / 1e3
 	}
 
-	return curr[0], curr[1], curr[2], nil
+	return res[0], res[1], res[2], nil
 }
 
 // voltages implements the api.PhaseVoltages interface
@@ -302,12 +302,12 @@ func (wb *BenderCC) voltages() (float64, float64, float64, error) {
 		return 0, 0, 0, err
 	}
 
-	var volt [3]float64
+	var res [3]float64
 	for l := 0; l < 3; l++ {
-		volt[l] = float64(binary.BigEndian.Uint32(b[4*l : 4*(l+1)]))
+		res[l] = float64(binary.BigEndian.Uint32(b[4*l : 4*(l+1)]))
 	}
 
-	return volt[0], volt[1], volt[2], nil
+	return res[0], res[1], res[2], nil
 }
 
 // identify implements the api.Identifier interface
diff --git a/charger/heidelberg-ec.go b/charger/heidelberg-ec.go
index ef6f8b05..8b007961 100644
--- a/charger/heidelberg-ec.go
+++ b/charger/heidelberg-ec.go
@@ -271,12 +271,12 @@ func (wb *HeidelbergEC) Currents() (float64, float64, float64, error) {
 		return 0, 0, 0, err
 	}
 
-	var curr [3]float64
+	var res [3]float64
 	for l := 0; l < 3; l++ {
-		curr[l] = float64(binary.BigEndian.Uint16(b[2*l:])) / 10
+		res[l] = float64(binary.BigEndian.Uint16(b[2*l:])) / 10
 	}
 
-	return curr[0], curr[1], curr[2], nil
+	return res[0], res[1], res[2], nil
 }
 
 var _ api.PhaseVoltages = (*HeidelbergEC)(nil)
@@ -288,12 +288,12 @@ func (wb *HeidelbergEC) Voltages() (float64, float64, float64, error) {
 		return 0, 0, 0, err
 	}
 
-	var volt [3]float64
+	var res [3]float64
 	for l := 0; l < 3; l++ {
-		volt[l] = float64(binary.BigEndian.Uint16(b[2*l:]))
+		res[l] = float64(binary.BigEndian.Uint16(b[2*l:]))
 	}
 
-	return volt[0], volt[1], volt[2], nil
+	return res[0], res[1], res[2], nil
 }
 
 var _ api.Diagnosis = (*HeidelbergEC)(nil)
diff --git a/charger/openwb.go b/charger/openwb.go
index 8f67e0ca..ff257ca3 100644
--- a/charger/openwb.go
+++ b/charger/openwb.go
@@ -214,17 +214,17 @@ var _ api.PhaseCurrents = (*OpenWB)(nil)
 
 // Currents implements the api.PhaseCurrents interface
 func (m *OpenWB) Currents() (float64, float64, float64, error) {
-	var currents []float64
+	var res []float64
 	for _, currentG := range m.currentsG {
 		c, err := currentG()
 		if err != nil {
 			return 0, 0, 0, err
 		}
 
-		currents = append(currents, c)
+		res = append(res, c)
 	}
 
-	return currents[0], currents[1], currents[2], nil
+	return res[0], res[1], res[2], nil
 }
 
 var _ api.Authorizer = (*OpenWB)(nil)
diff --git a/charger/smaevcharger.go b/charger/smaevcharger.go
index 37502204..f557b81c 100644
--- a/charger/smaevcharger.go
+++ b/charger/smaevcharger.go
@@ -252,18 +252,18 @@ var _ api.PhaseCurrents = (*Smaevcharger)(nil)
 
 // Currents implements the api.PhaseCurrents interface
 func (wb *Smaevcharger) Currents() (float64, float64, float64, error) {
-	var curr []float64
+	var res [3]float64
 
-	for _, phase := range []string{"A", "B", "C"} {
+	for i, phase := range []string{"A", "B", "C"} {
 		val, err := wb.getMeasurement("Measurement.GridMs.A.phs" + phase)
 		if err != nil {
 			return 0, 0, 0, err
 		}
 
-		curr = append(curr, -val)
+		res[i] = -val
 	}
 
-	return curr[0], curr[1], curr[2], nil
+	return res[0], res[1], res[2], nil
 }
 
 // reset cache
diff --git a/charger/vestel.go b/charger/vestel.go
index b4e1a223..38bb75d2 100644
--- a/charger/vestel.go
+++ b/charger/vestel.go
@@ -42,11 +42,13 @@ const (
 	vestelRegSessionEnergy   = 1502
 	vestelRegFailsafeTimeout = 2002
 	vestelRegAlive           = 6000
-	//vestelRegChargepointState = 1000
+	// vestelRegChargepointState = 1000
 )
 
-var vestelRegCurrents = []uint16{1008, 1010, 1012} // non-continuous uint16 registers!
-var vestelRegVoltages = []uint16{1014, 1016, 1018} // non-continuous uint16 registers!
+var (
+	vestelRegCurrents = []uint16{1008, 1010, 1012} // non-continuous uint16 registers!
+	vestelRegVoltages = []uint16{1014, 1016, 1018} // non-continuous uint16 registers!
+)
 
 // Vestel is an api.Charger implementation for Vestel/Hymes wallboxes with Ethernet (SW modells).
 // It uses Modbus TCP to communicate with the wallbox at modbus client id 255.
@@ -212,34 +214,34 @@ var _ api.PhaseCurrents = (*Vestel)(nil)
 
 // Currents implements the api.PhaseCurrents interface
 func (wb *Vestel) Currents() (float64, float64, float64, error) {
-	var currents []float64
-	for _, regCurrent := range vestelRegCurrents {
+	var res [3]float64
+	for i, regCurrent := range vestelRegCurrents {
 		b, err := wb.conn.ReadInputRegisters(regCurrent, 1)
 		if err != nil {
 			return 0, 0, 0, err
 		}
 
-		currents = append(currents, float64(binary.BigEndian.Uint16(b))/1e3)
+		res[i] = float64(binary.BigEndian.Uint16(b)) / 1e3
 	}
 
-	return currents[0], currents[1], currents[2], nil
+	return res[0], res[1], res[2], nil
 }
 
 var _ api.PhaseVoltages = (*Vestel)(nil)
 
 // Voltages implements the api.PhaseVoltages interface
 func (wb *Vestel) Voltages() (float64, float64, float64, error) {
-	var voltages []float64
-	for _, regVoltage := range vestelRegVoltages {
+	var res [3]float64
+	for i, regVoltage := range vestelRegVoltages {
 		b, err := wb.conn.ReadInputRegisters(regVoltage, 1)
 		if err != nil {
 			return 0, 0, 0, err
 		}
 
-		voltages = append(voltages, float64(binary.BigEndian.Uint16(b)))
+		res[i] = float64(binary.BigEndian.Uint16(b))
 	}
 
-	return voltages[0], voltages[1], voltages[2], nil
+	return res[0], res[1], res[2], nil
 }
 
 var _ api.Diagnosis = (*Vestel)(nil)
diff --git a/charger/wallbe.go b/charger/wallbe.go
index 8b5a1f4d..9f43c159 100644
--- a/charger/wallbe.go
+++ b/charger/wallbe.go
@@ -195,17 +195,17 @@ func (wb *Wallbe) totalEnergy() (float64, error) {
 
 // currents implements the api.PhaseCurrents interface
 func (wb *Wallbe) currents() (float64, float64, float64, error) {
-	var currents []float64
-	for _, regCurrent := range wbRegCurrents {
-		b, err := wb.conn.ReadInputRegisters(regCurrent, 2)
+	var res [3]float64
+	for i, reg := range wbRegCurrents {
+		b, err := wb.conn.ReadInputRegisters(reg, 2)
 		if err != nil {
 			return 0, 0, 0, err
 		}
 
-		currents = append(currents, rs485.RTUInt32ToFloat64Swapped(b))
+		res[i] = rs485.RTUInt32ToFloat64Swapped(b)
 	}
 
-	return currents[0], currents[1], currents[2], nil
+	return res[0], res[1], res[2], nil
 }
 
 var _ api.Diagnosis = (*Wallbe)(nil)
diff --git a/charger/webasto-next.go b/charger/webasto-next.go
index 6b2c7030..57871405 100644
--- a/charger/webasto-next.go
+++ b/charger/webasto-next.go
@@ -217,17 +217,17 @@ var _ api.PhaseCurrents = (*WebastoNext)(nil)
 
 // Currents implements the api.PhaseCurrents interface
 func (wb *WebastoNext) Currents() (float64, float64, float64, error) {
-	var curr [3]float64
+	var res [3]float64
 	for l := uint16(0); l < 3; l++ {
 		b, err := wb.conn.ReadInputRegisters(tqRegCurrents+2*l, 1)
 		if err != nil {
 			return 0, 0, 0, err
 		}
 
-		curr[l] = float64(binary.BigEndian.Uint16(b)) / 1e3
+		res[l] = float64(binary.BigEndian.Uint16(b)) / 1e3
 	}
 
-	return curr[0], curr[1], curr[2], nil
+	return res[0], res[1], res[2], nil
 }
 
 var _ api.Identifier = (*WebastoNext)(nil)
diff --git a/core/loadpoint.go b/core/loadpoint.go
index bd4ad667..4e6b58e7 100644
--- a/core/loadpoint.go
+++ b/core/loadpoint.go
@@ -174,7 +174,7 @@ type Loadpoint struct {
 	progress                *Progress      // Step-wise progress indicator
 
 	// session log
-	db      session.Database
+	db      *session.DB
 	session *session.Session
 
 	tasks *util.Queue[Task] // tasks to be executed
diff --git a/core/session/db.go b/core/session/db.go
index 9c3e70a8..6c910471 100644
--- a/core/session/db.go
+++ b/core/session/db.go
@@ -12,11 +12,6 @@ type DB struct {
 	name string
 }
 
-type Database interface {
-	New(startEnergy float64) *Session
-	Persist(session interface{})
-}
-
 // NewStore creates a session store
 func NewStore(name string, db *gorm.DB) (*DB, error) {
 	err := db.AutoMigrate(new(Session))
@@ -57,3 +52,31 @@ func (s *DB) Sessions() (Sessions, error) {
 	tx := s.db.Find(&res)
 	return res, tx.Error
 }
+
+func (s *DB) ClosePendingSessionsInHistory(chargeMeterTotal float64) error {
+	var res Sessions
+	if tx := s.db.Find(&res, map[string]interface{}{"finished": "0001-01-01 00:00:00+00:00", "Loadpoint": s.name}); tx.Error != nil {
+		return tx.Error
+	}
+
+	for _, session := range res {
+		var nextSession Session
+
+		var tx *gorm.DB
+		if tx = s.db.Limit(1).Order("ID").Find(&nextSession, "ID > ? AND Loadpoint = ?", session.ID, s.name); tx.Error != nil {
+			return tx.Error
+		}
+
+		if tx.RowsAffected == 0 {
+			// no successor, this is the most recent session and it is open
+			session.MeterStop = &chargeMeterTotal
+		} else {
+			session.MeterStop = nextSession.MeterStart
+		}
+
+		session.ChargedEnergy = *session.MeterStop - *session.MeterStart
+		s.Persist(session)
+	}
+
+	return nil
+}
diff --git a/core/site.go b/core/site.go
index f59f7349..125748f8 100644
--- a/core/site.go
+++ b/core/site.go
@@ -142,6 +142,10 @@ func NewSiteFromConfig(
 			if lp.db, err = session.NewStore(lp.Title(), db.Instance); err != nil {
 				return nil, err
 			}
+			// Fix any dangling history
+			if err := lp.db.ClosePendingSessionsInHistory(lp.chargeMeterTotal()); err != nil {
+				return nil, err
+			}
 
 			// NOTE: this requires stopSession to respect async access
 			shutdown.Register(lp.stopSession)
diff --git a/meter/sma.go b/meter/sma.go
index dd9e806b..3a8e35ad 100644
--- a/meter/sma.go
+++ b/meter/sma.go
@@ -125,12 +125,12 @@ func (sm *SMA) Currents() (float64, float64, float64, error) {
 		}
 	}
 
-	var currents [3]float64
+	var res [3]float64
 	for i, id := range []sunny.ValueID{sunny.CurrentL1, sunny.CurrentL2, sunny.CurrentL3} {
-		currents[i] = util.SignFromPower(sma.AsFloat(values[id]), powers[i])
+		res[i] = util.SignFromPower(sma.AsFloat(values[id]), powers[i])
 	}
 
-	return currents[0], currents[1], currents[2], err
+	return res[0], res[1], res[2], err
 }
 
 var _ api.PhaseVoltages = (*SMA)(nil)
@@ -139,12 +139,12 @@ var _ api.PhaseVoltages = (*SMA)(nil)
 func (sm *SMA) Voltages() (float64, float64, float64, error) {
 	values, err := sm.device.Values()
 
-	var voltages [3]float64
+	var res [3]float64
 	for i, id := range []sunny.ValueID{sunny.VoltageL1, sunny.VoltageL2, sunny.VoltageL3} {
-		voltages[i] = sma.AsFloat(values[id])
+		res[i] = sma.AsFloat(values[id])
 	}
 
-	return voltages[0], voltages[1], voltages[2], err
+	return res[0], res[1], res[2], err
 }
 
 var _ api.PhasePowers = (*SMA)(nil)
@@ -153,15 +153,15 @@ var _ api.PhasePowers = (*SMA)(nil)
 func (sm *SMA) Powers() (float64, float64, float64, error) {
 	values, err := sm.device.Values()
 
-	var powers [3]float64
+	var res [3]float64
 	for i, id := range []sunny.ValueID{sunny.ActivePowerPlusL1, sunny.ActivePowerPlusL2, sunny.ActivePowerPlusL3} {
-		powers[i] = sma.AsFloat(values[id])
+		res[i] = sma.AsFloat(values[id])
 	}
 	for i, id := range []sunny.ValueID{sunny.ActivePowerMinusL1, sunny.ActivePowerMinusL2, sunny.ActivePowerMinusL3} {
-		powers[i] -= sma.AsFloat(values[id])
+		res[i] -= sma.AsFloat(values[id])
 	}
 
-	return powers[0], powers[1], powers[2], err
+	return res[0], res[1], res[2], err
 }
 
 // soc implements the api.Battery interface


Test patch:
diff --git a/core/loadpoint_session_test.go b/core/loadpoint_session_test.go
index aeb63749..fdc9a1e7 100644
--- a/core/loadpoint_session_test.go
+++ b/core/loadpoint_session_test.go
@@ -87,3 +87,99 @@ func TestSession(t *testing.T) {
 	assert.Len(t, s, 1)
 	t.Logf("session: %+v", s)
 }
+
+func TestCloseSessionsOnStartup_emptyDb(t *testing.T) {
+	var err error
+	serverdb.Instance, err = serverdb.New("sqlite", ":memory:")
+	assert.NoError(t, err)
+
+	db, err := session.NewStore("foo", serverdb.Instance)
+	assert.NoError(t, err)
+
+	// assert empty DB is no problem
+	err = db.ClosePendingSessionsInHistory(1000)
+	assert.NoError(t, err)
+}
+
+func TestCloseSessionsOnStartup(t *testing.T) {
+	var err error
+	serverdb.Instance, err = serverdb.New("sqlite", ":memory:")
+	assert.NoError(t, err)
+
+	db1, err := session.NewStore("foo", serverdb.Instance)
+	assert.NoError(t, err)
+
+	db2, err := session.NewStore("bar", serverdb.Instance)
+	assert.NoError(t, err)
+
+	clock := clock.NewMock()
+
+	//test data, creates 6 sessions for each loadpoint, 3rd and 6th are "unfinished"
+	var sessions1 []*session.Session = createMockSessions(db1, clock)
+	var sessions2 []*session.Session = createMockSessions(db2, clock)
+
+	//write interleaved for two loadpoints
+	for index, session := range sessions1 {
+		db1.Persist(session)
+		db2.Persist(sessions2[index])
+	}
+
+	err = db1.ClosePendingSessionsInHistory(1000)
+	assert.NoError(t, err)
+
+	//check fixed sessions for db1
+	var db1Sessions session.Sessions
+	err = serverdb.Instance.Where("Loadpoint = ?", "foo").Order("ID").Find(&db1Sessions).Error
+	assert.NoError(t, err)
+	assert.Len(t, db1Sessions, 6)
+
+	//check fixed history
+	for _, s := range db1Sessions[:5] {
+		assert.NotEmpty(t, s.MeterStop)
+		assert.Equal(t, float64(10), s.ChargedEnergy)
+		t.Logf("session: %+v", s)
+	}
+
+	//check fixed most recent record
+	assert.NotEmpty(t, db1Sessions[5].MeterStop)
+	assert.Equal(t, float64(940), db1Sessions[5].ChargedEnergy)
+
+	//ensure no side effects on loadpoint 2 data, i.e. data left unfixed
+	var db2Sessions session.Sessions
+	err = serverdb.Instance.Where("Loadpoint = ?", "bar").Order("ID").Find(&db2Sessions).Error
+	assert.NoError(t, err)
+	assert.Len(t, db2Sessions, 6)
+
+	for i, s := range db2Sessions {
+		if (i+1)%3 == 0 {
+			assert.Empty(t, s.MeterStop)
+			assert.Empty(t, s.ChargedEnergy)
+			continue
+		}
+		assert.NotEmpty(t, s.MeterStop)
+		assert.Equal(t, float64(10), s.ChargedEnergy)
+	}
+}
+
+func createMockSessions(db *session.DB, clock *clock.Mock) []*session.Session {
+	var sessions []*session.Session
+	for i := 1; i <= 6; i++ {
+
+		var meter1Start float64 = float64(i * 10)
+		session := db.New(meter1Start)
+		session.Created = clock.Now().Add(1 * time.Minute)
+
+		//create every third session as incomplete
+		if i%3 == 0 {
+			sessions = append(sessions, session)
+			continue
+		}
+
+		session.Finished = clock.Now().Add(2 * time.Minute)
+		meterStop := float64(meter1Start + 10)
+		session.MeterStop = &meterStop
+		session.ChargedEnergy = 10
+		sessions = append(sessions, session)
+	}
+	return sessions
+}