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 // Lowest charge power (just before vehicle stops charging at 100%) maxChargePower = 50000.0 // default 50 kW maxChargeSoc = 50.0 // default 50% minChargeSoc = 100.0 gradient = (minChargePower - maxChargePower) / (minChargeSoc - maxChargeSoc) ) // Estimator provides vehicle soc and charge duration // Vehicle Soc can be estimated to provide more granularity type Estimator struct { log *util.Logger charger api.Charger vehicle api.Vehicle virtualCapacity float64 // estimated virtual vehicle capacity in Wh vehicleSoc float64 // estimated vehicle Soc initialSoc float64 // first received valid vehicle Soc initialEnergy float64 // energy counter at first valid Soc prevSoc float64 // previous vehicle Soc in % prevChargedEnergy float64 // previous charged energy in Wh energyPerSocStep float64 // Energy per Soc percent in Wh } // NewEstimator creates new estimator func NewEstimator(log *util.Logger, charger api.Charger, vehicle api.Vehicle) *Estimator { s := &Estimator{ log: log, charger: charger, vehicle: vehicle, } s.virtualCapacity = s.vehicle.Capacity() * 1e3 / ChargeEfficiency // initial capacity taking efficiency into account s.energyPerSocStep = s.virtualCapacity / 100 return s } // 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, virtualCapacity float64) time.Duration { return remainingChargeDuration(targetSoc, chargePower, vehicleSoc, virtualCapacity*1e3/ChargeEfficiency) } func remainingChargeDuration(targetSoc, chargePower, vehicleSoc, virtualCapacity float64) time.Duration { // Relativer Reduktionspunkt rrp := (chargePower-minChargePower)/gradient + minChargeSoc var t1, t2 float64 // Zeit von vehicleSoc bis Reduktionspunkt (linear) if vehicleSoc < rrp { t1 = (min(float64(targetSoc), rrp) - vehicleSoc) / minChargeSoc * virtualCapacity / chargePower } // Zeit von Reduktionspunkt bis targetSoc (degressiv) if float64(targetSoc) > rrp { t2 = (float64(targetSoc) - max(vehicleSoc, rrp)) / minChargeSoc * virtualCapacity / ((chargePower-minChargePower)/2 + minChargePower) } return max(0, time.Duration(float64(time.Hour)*(t1+t2))).Round(time.Second) } // RemainingChargeEnergy returns the remaining charge energy in kWh func (s *Estimator) RemainingChargeEnergy(targetSoc int) float64 { return remainingChargeEnergy(targetSoc, s.vehicleSoc, s.virtualCapacity) } func RemainingChargeEnergy(targetSoc int, vehicleSoc, capacity float64) float64 { return remainingChargeEnergy(targetSoc, vehicleSoc, capacity*1e3/ChargeEfficiency) } func remainingChargeEnergy(targetSoc int, vehicleSoc, virtualCapacity float64) float64 { percentRemaining := float64(targetSoc) - vehicleSoc if percentRemaining <= 0 || virtualCapacity <= 0 { return 0 } return percentRemaining / 100 * 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 { s.vehicleSoc = *fetchedSoc } else { s.log.WARN.Printf("missing vehicle soc- ignored by estimator") } socDelta := s.vehicleSoc - s.prevSoc energyDelta := max(chargedEnergy, 0) - s.prevChargedEnergy 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 = max(s.vehicle.Capacity()*1e3, 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 }