Session energy: latch baseline late if meter not ready at start (#29656)

This commit is contained in:
andig 2026-05-05 17:49:33 +02:00 • committed by GitHub
parent d4d8109a63
commit 338e8b4a46
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 79 additions and 9 deletions

View file

@ -21,7 +21,7 @@ type ChargeRater struct {
meter api.Meter
charging bool
start time.Time
startEnergy float64
startEnergy *float64 // nil until baseline successfully read from meter
chargedEnergy float64
}
@ -47,11 +47,12 @@ func (cr *ChargeRater) StartCharge(continued bool) {
// time is needed if MeterEnergy is not supported
cr.start = cr.clck.Now()
cr.startEnergy = nil
// get end energy amount
if m, ok := api.Cap[api.MeterEnergy](cr.meter); ok {
if f, err := m.TotalEnergy(); err == nil {
cr.startEnergy = f
cr.startEnergy = &f
cr.log.DEBUG.Printf("charge start energy: %.3fkWh", f)
} else if !loadpoint.AcceptableError(err) {
cr.log.ERROR.Printf("charge total import: %v", err)
@ -76,7 +77,9 @@ func (cr *ChargeRater) StopCharge() {
// get end energy amount
if m, ok := api.Cap[api.MeterEnergy](cr.meter); ok {
if f, err := m.TotalEnergy(); err == nil {
cr.chargedEnergy += f - cr.startEnergy
if cr.startEnergy != nil {
cr.chargedEnergy += f - *cr.startEnergy
}
cr.log.DEBUG.Printf("charge final energy: %.3fkWh", cr.chargedEnergy)
} else if !loadpoint.AcceptableError(err) {
cr.log.ERROR.Printf("charge total import: %v", err)
@ -94,10 +97,12 @@ func (cr *ChargeRater) ResetCharge() {
// get end energy amount
if m, ok := api.Cap[api.MeterEnergy](cr.meter); ok {
if f, err := m.TotalEnergy(); err == nil {
cr.chargedEnergy += f - cr.startEnergy
cr.log.DEBUG.Printf("charge final energy: %.3fkWh", cr.chargedEnergy)
if cr.startEnergy != nil {
cr.chargedEnergy += f - *cr.startEnergy
cr.log.DEBUG.Printf("charge final energy: %.3fkWh", cr.chargedEnergy)
}
cr.startEnergy = f
cr.startEnergy = &f
} else if !loadpoint.AcceptableError(err) {
cr.log.ERROR.Printf("charge total import: %v", err)
}
@ -139,11 +144,18 @@ func (cr *ChargeRater) ChargedEnergy() (float64, error) {
// get current energy amount
if m, ok := api.Cap[api.MeterEnergy](cr.meter); ok {
f, err := m.TotalEnergy()
if err == nil {
return cr.chargedEnergy + f - cr.startEnergy, nil
if err != nil {
return 0, fmt.Errorf("charge total import: %v", err)
}
return 0, fmt.Errorf("charge total import: %v", err)
// late-latch baseline if StartCharge could not read TotalEnergy
// (e.g. OCPP transaction recovery before first MeterValues frame)
if cr.startEnergy == nil {
cr.startEnergy = &f
cr.log.DEBUG.Printf("charge start energy: %.3fkWh", f)
}
return cr.chargedEnergy + f - *cr.startEnergy, nil
}
// return charged energy sofar if meter is not used

View file

@ -1,6 +1,7 @@
package wrapper
import (
"errors"
"testing"
"time"
@ -110,3 +111,60 @@ func TestWrappedMeter(t *testing.T) {
t.Errorf("energy: %.1f %v", f, err)
}
}
// TestDeferredBaseline covers the OCPP transaction-recovery case: the meter is
// not yet readable when StartCharge fires, so the baseline must be latched on
// the first successful TotalEnergy() read instead of defaulting to zero
// (which would cause the lifetime register to be reported as session energy).
func TestDeferredBaseline(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mm := api.NewMockMeter(ctrl)
me := api.NewMockMeterEnergy(ctrl)
type EnergyDecorator struct {
api.Meter
api.MeterEnergy
}
cm := &EnergyDecorator{Meter: mm, MeterEnergy: me}
cr := NewChargeRater(util.NewLogger("foo"), cm)
clck := clock.NewMock()
cr.clck = clck
// meter not yet available at StartCharge — recovered transaction before first MeterValues
me.EXPECT().TotalEnergy().Return(0.0, errors.New("not available"))
cr.StartCharge(true)
// first read also fails — must surface the error, not a bogus delta
me.EXPECT().TotalEnergy().Return(0.0, errors.New("not available"))
if _, err := cr.ChargedEnergy(); err == nil {
t.Errorf("expected error while meter unavailable")
}
// first successful read latches the baseline (lifetime register, e.g. 939 kWh)
me.EXPECT().TotalEnergy().Return(939.080, nil)
if f, err := cr.ChargedEnergy(); f != 0 || err != nil {
t.Errorf("expected 0 on baseline-latch read, got %.3f %v", f, err)
}
// subsequent reads return delta against the latched baseline
me.EXPECT().TotalEnergy().Return(942.080, nil)
if f, err := cr.ChargedEnergy(); f != 3 || err != nil {
t.Errorf("expected 3kWh delta, got %.3f %v", f, err)
}
me.EXPECT().TotalEnergy().Return(944.080, nil)
cr.StopCharge()
if f, err := cr.ChargedEnergy(); f != 5 || err != nil {
t.Errorf("final energy: %.1f %v", f, err)
}
}