diff --git a/api/api.go b/api/api.go index 8cdaf0c01..6c0098240 100644 --- a/api/api.go +++ b/api/api.go @@ -81,3 +81,8 @@ type Vehicle interface { Capacity() int64 ChargeState() (float64, error) } + +// ChargeRemainder provides current charge cycle duration +type ChargeRemainder interface { + RemainingTime() (time.Duration, error) +} diff --git a/cmd/dump.go b/cmd/dump.go index 70d97f157..5dce277e5 100644 --- a/cmd/dump.go +++ b/cmd/dump.go @@ -49,6 +49,14 @@ func dumpAPIs(v interface{}) { } } + if v, ok := v.(api.ChargeRemainder); ok { + if duration, err := v.RemainingTime(); err != nil { + fmt.Printf("Remaining: %v\n", err) + } else { + fmt.Printf("Remaining: %v\n", duration) + } + } + if v, ok := v.(api.Diagnosis); ok { fmt.Println("Diagnostic dump:") v.Diagnosis() diff --git a/core/wrapper/socestimator.go b/core/wrapper/socestimator.go index 05a8da58c..a0d17be91 100644 --- a/core/wrapper/socestimator.go +++ b/core/wrapper/socestimator.go @@ -51,6 +51,17 @@ func (s *SocEstimator) RemainingChargeDuration(chargePower float64, targetSoC in return 0 } + // use vehicle api if available + if vr, ok := s.vehicle.(api.ChargeRemainder); ok { + timeRemaining, err := vr.RemainingTime() + if err == nil { + return timeRemaining * time.Duration(percentRemaining/(100-s.socCharge)) + } + + s.log.WARN.Printf("updating remaining time failed: %v", err) + } + + // estimate remaining time whRemaining := percentRemaining / 100 * s.capacity return time.Duration(float64(time.Hour) * whRemaining / chargePower).Round(time.Second) } diff --git a/provider/cache.go b/provider/cache.go index a35988725..07978c114 100644 --- a/provider/cache.go +++ b/provider/cache.go @@ -95,3 +95,20 @@ func (c *Cached) BoolGetter() func() (bool, error) { return c.val.(bool), c.err } } + +// DurationGetter gets time.Duration value +func (c *Cached) DurationGetter() func() (time.Duration, error) { + g, ok := c.getter.(func() (time.Duration, error)) + if !ok { + c.log.FATAL.Fatalf("invalid type: %T", c.getter) + } + + return func() (time.Duration, error) { + if c.clock.Since(c.updated) > c.cache { + c.val, c.err = g() + c.updated = c.clock.Now() + } + + return c.val.(time.Duration), c.err + } +} diff --git a/vehicle/renault.go b/vehicle/renault.go index f6b40a16d..b38b11cc9 100644 --- a/vehicle/renault.go +++ b/vehicle/renault.go @@ -72,6 +72,7 @@ type kamereonData struct { } type batteryAttributes struct { + Timestamp string `json:"timestamp"` ChargeStatus int `json:"chargeStatus"` InstantaneousPower int `json:"instantaneousPower"` RangeHvacOff int `json:"rangeHvacOff"` @@ -80,6 +81,7 @@ type batteryAttributes struct { PlugStatus int `json:"plugStatus"` LastUpdateTime string `json:"lastUpdateTime"` ChargePower int `json:"chargePower"` + RemainingTime *int `json:"chargingRemainingTime"` } // Renault is an api.Vehicle implementation for Renault cars @@ -91,6 +93,7 @@ type Renault struct { gigyaJwtToken string accountID string chargeStateG func() (float64, error) + remainingTimeG func() (time.Duration, error) } func init() { @@ -131,6 +134,7 @@ func NewRenaultFromConfig(other map[string]interface{}) (api.Vehicle, error) { } v.chargeStateG = provider.NewCached(v.chargeState, cc.Cache).FloatGetter() + v.remainingTimeG = provider.NewCached(v.remainingTime, cc.Cache).DurationGetter() return v, nil } @@ -302,7 +306,7 @@ func (v *Renault) kamereonVehicles(accountID string) (string, error) { // chargeState implements the Vehicle.ChargeState interface func (v *Renault) chargeState() (float64, error) { - uri := fmt.Sprintf("%s/commerce/v1/accounts/%s/kamereon/kca/car-adapter/v1/cars/%s/battery-status", v.kamereon.Target, v.accountID, v.vin) + uri := fmt.Sprintf("%s/commerce/v1/accounts/%s/kamereon/kca/car-adapter/v2/cars/%s/battery-status", v.kamereon.Target, v.accountID, v.vin) kr, err := v.kamereonRequest(uri) // repeat auth if error @@ -319,3 +323,32 @@ func (v *Renault) chargeState() (float64, error) { func (v *Renault) ChargeState() (float64, error) { return v.chargeStateG() } + +// remainingTime implements the Vehicle.ChargeRemainder interface +func (v *Renault) remainingTime() (time.Duration, error) { + uri := fmt.Sprintf("%s/commerce/v1/accounts/%s/kamereon/kca/car-adapter/v2/cars/%s/battery-status", v.kamereon.Target, v.accountID, v.vin) + kr, err := v.kamereonRequest(uri) + + // repeat auth if error + if err != nil { + if err = v.authFlow(); err == nil { + kr, err = v.kamereonRequest(uri) + } + } + + var timestamp time.Time + if err == nil { + timestamp, err = time.Parse(time.RFC3339, kr.Data.Attributes.Timestamp) + } + + if kr.Data.Attributes.RemainingTime == nil { + return 0, errors.New("estimate not available") + } + + return time.Duration(*kr.Data.Attributes.RemainingTime)*time.Minute - time.Since(timestamp), err +} + +// RemainingTime implements the Vehicle.ChargeRemainder interface +func (v *Renault) RemainingTime() (time.Duration, error) { + return v.remainingTimeG() +}