139 lines
5.2 KiB
Go
139 lines
5.2 KiB
Go
package soc
|
|
|
|
import (
|
|
"time"
|
|
|
|
"github.com/evcc-io/evcc/api"
|
|
"github.com/evcc-io/evcc/util"
|
|
)
|
|
|
|
const (
|
|
ChargeEfficiency = 0.85 // assume 85% charge efficiency
|
|
|
|
minChargePower = 1000.0 // charge power at 100% soc (just before the vehicle stops charging)
|
|
maxChargePower = 50000.0 // charge power up to maxChargeSoc
|
|
maxChargeSoc = 50.0 // soc up to which maxChargePower is available
|
|
|
|
// power reduction per soc percent above maxChargeSoc
|
|
powerPerSoc = (maxChargePower - minChargePower) / (100 - maxChargeSoc)
|
|
)
|
|
|
|
// Estimator provides vehicle soc and charge duration
|
|
// Vehicle Soc can be estimated to provide more granularity
|
|
type Estimator struct {
|
|
log *util.Logger
|
|
|
|
capacity float64 // vehicle capacity in Wh
|
|
energyPerSocStep float64 // energy per soc percent in Wh
|
|
vehicleSoc float64 // estimated vehicle soc in %
|
|
initialSoc float64 // first received valid vehicle soc in %
|
|
initialEnergy float64 // energy counter at first valid soc in Wh
|
|
prevSoc float64 // vehicle soc at last soc change in %
|
|
prevChargedEnergy float64 // charged energy at last soc change in Wh
|
|
sampled bool // a valid vehicle soc was received
|
|
}
|
|
|
|
// NewEstimator creates new estimator
|
|
func NewEstimator(log *util.Logger, vehicle api.Vehicle) *Estimator {
|
|
capacity := vehicle.Capacity() * 1e3
|
|
|
|
return &Estimator{
|
|
log: log,
|
|
capacity: capacity,
|
|
energyPerSocStep: capacity / ChargeEfficiency / 100, // initial gradient taking efficiency into account
|
|
}
|
|
}
|
|
|
|
// virtualCapacity returns the estimated capacity in Wh, never below the vehicle's physical capacity
|
|
func (s *Estimator) virtualCapacity() float64 {
|
|
return max(s.capacity, s.energyPerSocStep*100)
|
|
}
|
|
|
|
// RemainingChargeDuration returns the estimated remaining duration
|
|
func (s *Estimator) RemainingChargeDuration(targetSoc, chargePower float64) time.Duration {
|
|
return remainingChargeDuration(targetSoc, chargePower, s.vehicleSoc, s.virtualCapacity())
|
|
}
|
|
|
|
func RemainingChargeDuration(targetSoc, chargePower, vehicleSoc, capacity float64) time.Duration {
|
|
return remainingChargeDuration(targetSoc, chargePower, vehicleSoc, capacity*1e3/ChargeEfficiency)
|
|
}
|
|
|
|
func remainingChargeDuration(targetSoc, chargePower, vehicleSoc, virtualCapacity float64) time.Duration {
|
|
// soc above which charge power starts to taper off
|
|
taperSoc := 100 - (chargePower-minChargePower)/powerPerSoc
|
|
|
|
var hours float64
|
|
|
|
// below the taper point the vehicle charges at full power
|
|
if vehicleSoc < taperSoc {
|
|
hours += (min(targetSoc, taperSoc) - vehicleSoc) / 100 * virtualCapacity / chargePower
|
|
}
|
|
|
|
// above the taper point power decreases linearly towards minChargePower
|
|
if targetSoc > taperSoc {
|
|
hours += (targetSoc - max(vehicleSoc, taperSoc)) / 100 * virtualCapacity / ((chargePower + minChargePower) / 2)
|
|
}
|
|
|
|
return max(0, time.Duration(float64(time.Hour)*hours)).Round(time.Second)
|
|
}
|
|
|
|
// RemainingChargeEnergy returns the remaining charge energy in kWh
|
|
func (s *Estimator) RemainingChargeEnergy(targetSoc int) float64 {
|
|
return remainingChargeEnergy(float64(targetSoc), s.vehicleSoc, s.virtualCapacity())
|
|
}
|
|
|
|
func RemainingChargeEnergy(targetSoc int, vehicleSoc, capacity float64) float64 {
|
|
return remainingChargeEnergy(float64(targetSoc), vehicleSoc, capacity*1e3/ChargeEfficiency)
|
|
}
|
|
|
|
func remainingChargeEnergy(targetSoc, vehicleSoc, virtualCapacity float64) float64 {
|
|
return max(0, targetSoc-vehicleSoc) / 100 * max(0, virtualCapacity) / 1e3
|
|
}
|
|
|
|
// Soc replaces the api.Vehicle.Soc interface to take charged energy into account
|
|
func (s *Estimator) Soc(fetchedSoc *float64, chargedEnergy float64) float64 {
|
|
if fetchedSoc == nil {
|
|
// extrapolate soc from charged energy while no vehicle soc is available,
|
|
// never below the current estimate to stay monotonic across energy resets
|
|
if energyDelta := max(chargedEnergy, 0) - s.prevChargedEnergy; s.sampled && energyDelta >= 0 {
|
|
s.vehicleSoc = min(max(s.vehicleSoc, s.prevSoc+energyDelta/s.energyPerSocStep), 100)
|
|
s.log.DEBUG.Printf("soc extrapolated: %.2f%%", s.vehicleSoc)
|
|
}
|
|
return s.vehicleSoc
|
|
}
|
|
|
|
chargedEnergy = max(chargedEnergy, 0)
|
|
socDelta := *fetchedSoc - s.prevSoc
|
|
energyDelta := chargedEnergy - s.prevChargedEnergy
|
|
|
|
// no soc change and no energy reset: interpolate soc from charged energy.
|
|
// the first valid soc always takes the sampling path below to seed the baseline.
|
|
if s.sampled && socDelta == 0 && energyDelta >= 0 {
|
|
s.vehicleSoc = min(*fetchedSoc+energyDelta/s.energyPerSocStep, 100)
|
|
s.log.DEBUG.Printf("soc estimated: %.2f%% (vehicle: %.2f%%)", s.vehicleSoc, *fetchedSoc)
|
|
return s.vehicleSoc
|
|
}
|
|
|
|
s.sampled = true
|
|
s.vehicleSoc = *fetchedSoc
|
|
|
|
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.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
|
|
s.prevSoc = s.vehicleSoc
|
|
s.prevChargedEnergy = chargedEnergy
|
|
|
|
return s.vehicleSoc
|
|
}
|