Loadpoint: simplify estimator (#26956)

This commit is contained in:
andig 2026-01-25 13:09:44 +01:00 • committed by GitHub
parent 766b314efe
commit ae9a64984f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 39 additions and 80 deletions

View file

@ -506,11 +506,6 @@ func (lp *Loadpoint) evVehicleConnectHandler() {
// soc update reset
lp.socUpdated = time.Time{}
// soc update reset on car change
if lp.socEstimator != nil {
lp.socEstimator.Reset()
}
// set default or start detection
if !lp.chargerHasFeature(api.IntegratedDevice) {
lp.vehicleDefaultOrDetect()
@ -1780,7 +1775,7 @@ func (lp *Loadpoint) publishSocAndRange() {
if socEstimator == nil {
lp.vehicleSoc = *soc
} else {
lp.vehicleSoc, _ = socEstimator.Soc(soc, lp.GetChargedEnergy())
lp.vehicleSoc = socEstimator.Soc(soc, lp.GetChargedEnergy())
lp.log.DEBUG.Printf("vehicle soc (estimator): %.0f%%", lp.vehicleSoc)
}
}

6
core/soc/README.md Normal file
View file

@ -0,0 +1,6 @@
| fetchedSoc | chargedEnergy | result |
| ---------- | ------------- | --------------------------------------------------- |
| nil | <=0 | 0 |
| nil | value | prevsoc + delta |
| value | <=0 | initialsoc setzen |
| value | value | initialsoc/initialenergy setzen falls nicht gesetzt |

View file

@ -25,7 +25,6 @@ type Estimator struct {
charger api.Charger
vehicle api.Vehicle
capacity float64 // vehicle capacity in Wh cached to simplify testing
virtualCapacity float64 // estimated virtual vehicle capacity in Wh
vehicleSoc float64 // estimated vehicle Soc
initialSoc float64 // first received valid vehicle Soc
@ -43,21 +42,12 @@ func NewEstimator(log *util.Logger, charger api.Charger, vehicle api.Vehicle) *E
vehicle: vehicle,
}
s.Reset()
s.virtualCapacity = s.vehicle.Capacity() * 1e3 / ChargeEfficiency // initial capacity taking efficiency into account
s.energyPerSocStep = s.virtualCapacity / 100
return s
}
// Reset resets the estimation process to default values
func (s *Estimator) Reset() {
s.prevSoc = 0
s.prevChargedEnergy = 0
s.initialSoc = 0
s.capacity = s.vehicle.Capacity() * 1e3 // cache to simplify debugging
s.virtualCapacity = s.capacity / ChargeEfficiency // initial capacity taking efficiency into account
s.energyPerSocStep = s.virtualCapacity / 100
}
// RemainingChargeDuration returns the estimated remaining duration
func (s *Estimator) RemainingChargeDuration(targetSoc, chargePower float64) time.Duration {
return remainingChargeDuration(targetSoc, chargePower, s.vehicleSoc, s.virtualCapacity)
@ -99,60 +89,39 @@ func (s *Estimator) RemainingChargeEnergy(targetSoc int) float64 {
}
// Soc replaces the api.Vehicle.Soc interface to take charged energy into account
func (s *Estimator) Soc(fetchedSoc *float64, chargedEnergy float64) (float64, error) {
func (s *Estimator) Soc(fetchedSoc *float64, chargedEnergy float64) float64 {
if fetchedSoc != nil {
s.vehicleSoc = *fetchedSoc
} else {
s.log.WARN.Printf("missing vehicle soc- ignored by estimator")
}
if s.virtualCapacity > 0 {
socDelta := s.vehicleSoc - s.prevSoc
energyDelta := max(chargedEnergy, 0) - s.prevChargedEnergy
socDelta := s.vehicleSoc - s.prevSoc
energyDelta := max(chargedEnergy, 0) - s.prevChargedEnergy
if socDelta != 0 || energyDelta < 0 { // soc value change or unexpected energy reset
// compare ChargeState of vehicle and charger
var invalid bool
if vs, ok := s.vehicle.(api.ChargeState); ok {
ccs, err := s.charger.Status()
if err != nil {
return 0, err
}
vcs, err := vs.Status()
if err != nil {
vcs = ccs // sanitize vehicle errors
} else {
s.log.DEBUG.Printf("vehicle status: %s", vcs)
}
invalid = vcs != ccs
}
if !invalid {
if s.initialSoc == 0 {
s.initialSoc = s.vehicleSoc
s.initialEnergy = chargedEnergy
}
socDiff := s.vehicleSoc - s.initialSoc
energyDiff := chargedEnergy - s.initialEnergy
// recalculate gradient, wh per soc %
if socDiff > 10 && energyDiff > 0 {
s.energyPerSocStep = energyDiff / socDiff
s.virtualCapacity = s.energyPerSocStep * 100
s.log.DEBUG.Printf("soc gradient updated: soc: %.1f%%, socDiff: %.1f%%, energyDiff: %.0fWh, energyPerSocStep: %.1fWh, virtualCapacity: %.0fWh", s.vehicleSoc, socDiff, energyDiff, s.energyPerSocStep, s.virtualCapacity)
}
}
// sample charged energy at soc change, reset energy delta
s.prevChargedEnergy = max(chargedEnergy, 0)
s.prevSoc = s.vehicleSoc
} else {
s.vehicleSoc = min(*fetchedSoc+energyDelta/s.energyPerSocStep, 100)
s.log.DEBUG.Printf("soc estimated: %.2f%% (vehicle: %.2f%%)", s.vehicleSoc, *fetchedSoc)
if socDelta != 0 || energyDelta < 0 { // soc value change or unexpected energy reset
if s.initialSoc == 0 {
s.initialSoc = s.vehicleSoc
s.initialEnergy = chargedEnergy
}
socDiff := s.vehicleSoc - s.initialSoc
energyDiff := chargedEnergy - s.initialEnergy
// recalculate gradient, wh per soc %
if socDiff > 10 && energyDiff > 0 {
s.energyPerSocStep = energyDiff / socDiff
s.virtualCapacity = s.energyPerSocStep * 100
s.log.DEBUG.Printf("soc gradient updated: soc: %.1f%%, socDiff: %.1f%%, energyDiff: %.0fWh, energyPerSocStep: %.1fWh, virtualCapacity: %.0fWh", s.vehicleSoc, socDiff, energyDiff, s.energyPerSocStep, s.virtualCapacity)
}
// sample charged energy at soc change, reset energy delta
s.prevChargedEnergy = max(chargedEnergy, 0)
s.prevSoc = s.vehicleSoc
} else {
s.vehicleSoc = min(*fetchedSoc+energyDelta/s.energyPerSocStep, 100)
s.log.DEBUG.Printf("soc estimated: %.2f%% (vehicle: %.2f%%)", s.vehicleSoc, *fetchedSoc)
}
return s.vehicleSoc, nil
return s.vehicleSoc
}

View file

@ -71,20 +71,11 @@ func TestSocEstimation(t *testing.T) {
for _, tc := range tc {
t.Logf("%+v", tc)
soc, err := ce.Soc(&tc.vehicleSoc, tc.chargedEnergy)
if err != nil {
t.Error(err)
}
soc := ce.Soc(&tc.vehicleSoc, tc.chargedEnergy)
// validate soc estimate
if tc.estimatedSoc != soc {
t.Errorf("expected estimated soc: %g, got: %g", tc.estimatedSoc, soc)
}
// validate capacity estimate
if tc.virtualCapacity != ce.virtualCapacity {
t.Errorf("expected virtual capacity: %v, got: %v", tc.virtualCapacity, ce.virtualCapacity)
}
// validate soc/capacity estimate
assert.Equal(t, tc.estimatedSoc, soc, "estimated soc")
assert.Equal(t, tc.virtualCapacity, ce.virtualCapacity, "virtual capacity")
// validate duration estimate
chargePower := 1e3
@ -92,9 +83,7 @@ func TestSocEstimation(t *testing.T) {
remainingHours := (float64(targetSoc) - soc) / 100 * tc.virtualCapacity / chargePower
remainingDuration := time.Duration(float64(time.Hour) * remainingHours).Round(time.Second)
if rm := ce.RemainingChargeDuration(targetSoc, chargePower); rm != remainingDuration {
t.Errorf("expected estimated duration: %v, got: %v", remainingDuration, rm)
}
assert.Equal(t, remainingDuration, ce.RemainingChargeDuration(targetSoc, chargePower), "remaining duration")
}
}