diff --git a/api/api.go b/api/api.go index eb39b9be4..68b936a22 100644 --- a/api/api.go +++ b/api/api.go @@ -126,6 +126,11 @@ type VehicleClimater interface { Climater() (active bool, outsideTemp float64, targetTemp float64, err error) } +// VehicleOdometer returns the vehicles milage +type VehicleOdometer interface { + Odometer() (float64, error) +} + // VehicleStartCharge starts the charging session on the vehicle side type VehicleStartCharge interface { StartCharge() error diff --git a/cmd/dumper.go b/cmd/dumper.go index d56821536..216f02160 100644 --- a/cmd/dumper.go +++ b/cmd/dumper.go @@ -128,6 +128,14 @@ func (d *dumper) Dump(name string, v interface{}) { } } + if v, ok := v.(api.VehicleOdometer); ok { + if odo, err := v.Odometer(); err != nil { + fmt.Fprintf(w, "Odometer:\t%.0f\n", err) + } else { + fmt.Fprintf(w, "Odometer:\t%vkm\n", odo) + } + } + if v, ok := v.(api.VehicleFinishTimer); ok { if ft, err := v.FinishTime(); err != nil { fmt.Fprintf(w, "Finish time:\t%v\n", err) diff --git a/core/loadpoint.go b/core/loadpoint.go index 6f4e705c6..fae4154a2 100644 --- a/core/loadpoint.go +++ b/core/loadpoint.go @@ -133,6 +133,8 @@ type LoadPoint struct { socCharge float64 // Vehicle SoC chargedEnergy float64 // Charged energy while connected in Wh chargeDuration time.Duration // Charge duration + + tasks []func() error // task list for repeated execution } // NewLoadPointFromConfig creates a new loadpoint @@ -710,12 +712,15 @@ func (lp *LoadPoint) setActiveVehicle(vehicle api.Vehicle) { lp.publish("hasVehicle", true) lp.publish("socTitle", lp.vehicle.Title()) lp.publish("socCapacity", lp.vehicle.Capacity()) + + lp.task(lp.odometer) } else { lp.socEstimator = nil lp.publish("hasVehicle", false) lp.publish("socTitle", "") - lp.publish("socCapacity", 0) + lp.publish("socCapacity", int64(0)) + lp.publish("socOdometer", 0.0) } } @@ -1243,6 +1248,9 @@ func (lp *LoadPoint) Update(sitePower float64, cheap bool) { } } + // odometer etc, if active + lp.runTasks() + // publish soc after updating charger status to make sure // initial update of connected state matches charger status lp.publishSoCAndRange() diff --git a/core/loadpoint_task.go b/core/loadpoint_task.go new file mode 100644 index 000000000..402bc7359 --- /dev/null +++ b/core/loadpoint_task.go @@ -0,0 +1,41 @@ +package core + +import ( + "errors" + + "github.com/evcc-io/evcc/api" +) + +// task adds a task to the list of running tasks +func (lp *LoadPoint) task(task func() error) { + lp.tasks = append(lp.tasks, task) +} + +// runTasks runs all defined tasks +func (lp *LoadPoint) runTasks() { + var incomplete []func() error + for _, task := range lp.tasks { + err := task() + if errors.Is(err, api.ErrMustRetry) { + incomplete = append(incomplete, task) + } + } + lp.tasks = incomplete +} + +func (lp *LoadPoint) odometer() error { + v, ok := lp.vehicle.(api.VehicleOdometer) + if !ok { + return nil + } + + odo, err := v.Odometer() + switch err { + case nil: + lp.publish("socOdometer", odo) + case api.ErrMustRetry: + default: + lp.log.ERROR.Printf("vehicle odometer: %v", err) + } + return err +} diff --git a/vehicle/bmw/provider.go b/vehicle/bmw/provider.go index b173a6efd..e2d58ee3e 100644 --- a/vehicle/bmw/provider.go +++ b/vehicle/bmw/provider.go @@ -69,11 +69,23 @@ func (v *Provider) Status() (api.ChargeStatus, error) { var _ api.VehicleRange = (*Provider)(nil) // Range implements the api.VehicleRange interface -func (v *Provider) Range() (rng int64, err error) { +func (v *Provider) Range() (int64, error) { res, err := v.statusG() if res, ok := res.(StatusResponse); err == nil && ok { - rng = int64(res.VehicleStatus.RemainingRangeElectric) + return int64(res.VehicleStatus.RemainingRangeElectric), nil } - return rng, err + return 0, err +} + +var _ api.VehicleOdometer = (*Provider)(nil) + +// Odometer implements the api.VehicleOdometer interface +func (v *Provider) Odometer() (float64, error) { + res, err := v.statusG() + if res, ok := res.(StatusResponse); err == nil && ok { + return float64(res.VehicleStatus.Mileage), nil + } + + return 0, err } diff --git a/vehicle/porsche/provider.go b/vehicle/porsche/provider.go index e533d26e4..c273dc5b6 100644 --- a/vehicle/porsche/provider.go +++ b/vehicle/porsche/provider.go @@ -111,3 +111,15 @@ func (v *Provider) Range() (int64, error) { return 0, err } + +var _ api.VehicleOdometer = (*Provider)(nil) + +// Odometer implements the api.VehicleOdometer interface +func (v *Provider) Odometer() (float64, error) { + res, err := v.statusG() + if res, ok := res.(StatusResponse); err == nil && ok { + return res.CarControlData.Mileage.Value, nil + } + + return 0, err +} diff --git a/vehicle/psa/api.go b/vehicle/psa/api.go index 1b6d217fb..0c4bf2fc5 100644 --- a/vehicle/psa/api.go +++ b/vehicle/psa/api.go @@ -95,7 +95,10 @@ type Status struct { Status string // Disabled } } - Energy []Energy + Energy []Energy + Odometer struct { + Mileage float64 + } `json:"timed.odometer"` } // Energy is the /status partial energy response diff --git a/vehicle/psa/provider.go b/vehicle/psa/provider.go index 1d5cfd6a7..20b9b005d 100644 --- a/vehicle/psa/provider.go +++ b/vehicle/psa/provider.go @@ -63,6 +63,19 @@ func (v *Provider) Range() (int64, error) { return 0, err } +var _ api.VehicleOdometer = (*Provider)(nil) + +// Odometer implements the api.VehicleOdometer interface +func (v *Provider) Odometer() (float64, error) { + res, err := v.statusG() + + if res, ok := res.(Status); err == nil && ok { + return res.Odometer.Mileage, nil + } + + return 0, err +} + var _ api.VehicleFinishTimer = (*Provider)(nil) // FinishTime implements the api.VehicleFinishTimer interface diff --git a/vehicle/renault.go b/vehicle/renault.go index 6b7045d3c..8d49ad6d2 100644 --- a/vehicle/renault.go +++ b/vehicle/renault.go @@ -92,6 +92,8 @@ type attributes struct { // hvac-status ExternalTemperature float64 `json:"externalTemperature"` HvacStatus string `json:"hvacStatus"` + // cockpit + TotalMileage float64 `json:"totalMileage"` } // Renault is an api.Vehicle implementation for Renault cars @@ -103,6 +105,7 @@ type Renault struct { gigyaJwtToken string accountID string batteryG func() (interface{}, error) + cockpitG func() (interface{}, error) hvacG func() (interface{}, error) } @@ -148,6 +151,7 @@ func NewRenaultFromConfig(other map[string]interface{}) (api.Vehicle, error) { } v.batteryG = provider.NewCached(v.batteryAPI, cc.Cache).InterfaceGetter() + v.cockpitG = provider.NewCached(v.cockpitAPI, cc.Cache).InterfaceGetter() v.hvacG = provider.NewCached(v.hvacAPI, cc.Cache).InterfaceGetter() return v, err @@ -337,6 +341,21 @@ func (v *Renault) hvacAPI() (interface{}, error) { return res, err } +// cockpitAPI provides cockpit api response +func (v *Renault) cockpitAPI() (interface{}, error) { + uri := fmt.Sprintf("%s/commerce/v1/accounts/%s/kamereon/kca/car-adapter/v2/cars/%s/cockpit", v.kamereon.Target, v.accountID, v.vin) + res, err := v.kamereonRequest(uri) + + // repeat auth if error + if err != nil { + if err = v.authFlow(); err == nil { + res, err = v.kamereonRequest(uri) + } + } + + return res, err +} + // SoC implements the api.Vehicle interface func (v *Renault) SoC() (float64, error) { res, err := v.batteryG() @@ -380,6 +399,19 @@ func (v *Renault) Range() (int64, error) { return 0, err } +var _ api.VehicleOdometer = (*Renault)(nil) + +// Odometer implements the api.VehicleOdometer interface +func (v *Renault) Odometer() (float64, error) { + res, err := v.cockpitG() + + if res, ok := res.(kamereonResponse); err == nil && ok { + return res.Data.Attributes.TotalMileage, nil + } + + return 0, err +} + var _ api.VehicleFinishTimer = (*Renault)(nil) // FinishTime implements the api.VehicleFinishTimer interface diff --git a/vehicle/tesla.go b/vehicle/tesla.go index 2be29fa13..73a1b9e59 100644 --- a/vehicle/tesla.go +++ b/vehicle/tesla.go @@ -19,7 +19,7 @@ type Tesla struct { *embed vehicle *tesla.Vehicle chargeStateG func() (interface{}, error) - climateStateG func() (interface{}, error) + vehicleStateG func() (interface{}, error) } func init() { @@ -89,7 +89,7 @@ func NewTeslaFromConfig(other map[string]interface{}) (api.Vehicle, error) { } v.chargeStateG = provider.NewCached(v.chargeState, cc.Cache).InterfaceGetter() - v.climateStateG = provider.NewCached(v.climateState, cc.Cache).InterfaceGetter() + v.vehicleStateG = provider.NewCached(v.vehicleState, cc.Cache).InterfaceGetter() return v, nil } @@ -99,9 +99,9 @@ func (v *Tesla) chargeState() (interface{}, error) { return v.vehicle.ChargeState() } -// climateState implements the climater api -func (v *Tesla) climateState() (interface{}, error) { - return v.vehicle.ClimateState() +// vehicleState implements the climater api +func (v *Tesla) vehicleState() (interface{}, error) { + return v.vehicle.VehicleState() } // SoC implements the api.Vehicle interface @@ -147,6 +147,8 @@ func (v *Tesla) ChargedEnergy() (float64, error) { return 0, err } +const kmPerMile = 1.609344 + var _ api.VehicleRange = (*Tesla)(nil) // Range implements the api.VehicleRange interface @@ -155,7 +157,21 @@ func (v *Tesla) Range() (int64, error) { if res, ok := res.(*tesla.ChargeState); err == nil && ok { // miles to km - return int64(1.609344 * res.EstBatteryRange), nil + return int64(kmPerMile * res.EstBatteryRange), nil + } + + return 0, err +} + +var _ api.VehicleOdometer = (*Tesla)(nil) + +// Odometer implements the api.VehicleOdometer interface +func (v *Tesla) Odometer() (float64, error) { + res, err := v.vehicleStateG() + + if res, ok := res.(*tesla.VehicleState); err == nil && ok { + // miles to km + return kmPerMile * res.Odometer, nil } return 0, err diff --git a/vehicle/tronity.go b/vehicle/tronity.go index 1293b66f3..9cfd01467 100644 --- a/vehicle/tronity.go +++ b/vehicle/tronity.go @@ -217,6 +217,19 @@ func (v *Tronity) Range() (int64, error) { return 0, err } +var _ api.VehicleOdometer = (*Tronity)(nil) + +// Odometer implements the api.VehicleOdometer interface +func (v *Tronity) Odometer() (float64, error) { + res, err := v.bulkG() + + if res, ok := res.(tronity.Bulk); err == nil && ok { + return res.Odometer, nil + } + + return 0, err +} + func (v *Tronity) post(uri string) error { resp, err := v.Post(uri, "", nil) if err == nil { diff --git a/vehicle/volvo.go b/vehicle/volvo.go index 9e7b6cfff..510c15722 100644 --- a/vehicle/volvo.go +++ b/vehicle/volvo.go @@ -66,20 +66,20 @@ type volvoStatus struct { TimeToHVBatteryFullyCharged int `json:"timeToHVBatteryFullyCharged"` TimeToHVBatteryFullyChargedTimestamp string `json:"timeToHVBatteryFullyChargedTimestamp"` } `json:"hvBattery"` - Odometer int `json:"odometer"` - OdometerTimestamp string `json:"odometerTimestamp"` - PrivacyPolicyEnabled bool `json:"privacyPolicyEnabled"` - PrivacyPolicyEnabledTimestamp string `json:"privacyPolicyEnabledTimestamp"` - RemoteClimatizationStatus string `json:"remoteClimatizationStatus"` // CableConnectedWithoutPower - RemoteClimatizationStatusTimestamp string `json:"remoteClimatizationStatusTimestamp"` - ServiceWarningStatus string `json:"serviceWarningStatus"` - ServiceWarningStatusTimestamp string `json:"serviceWarningStatusTimestamp"` - TimeFullyAccessibleUntil string `json:"timeFullyAccessibleUntil"` - TimePartiallyAccessibleUntil string `json:"timePartiallyAccessibleUntil"` - TripMeter1 int `json:"tripMeter1"` - TripMeter1Timestamp string `json:"tripMeter1Timestamp"` - TripMeter2 int `json:"tripMeter2"` - TripMeter2Timestamp string `json:"tripMeter2Timestamp"` + Odometer float64 `json:"odometer"` + OdometerTimestamp string `json:"odometerTimestamp"` + PrivacyPolicyEnabled bool `json:"privacyPolicyEnabled"` + PrivacyPolicyEnabledTimestamp string `json:"privacyPolicyEnabledTimestamp"` + RemoteClimatizationStatus string `json:"remoteClimatizationStatus"` // CableConnectedWithoutPower + RemoteClimatizationStatusTimestamp string `json:"remoteClimatizationStatusTimestamp"` + ServiceWarningStatus string `json:"serviceWarningStatus"` + ServiceWarningStatusTimestamp string `json:"serviceWarningStatusTimestamp"` + TimeFullyAccessibleUntil string `json:"timeFullyAccessibleUntil"` + TimePartiallyAccessibleUntil string `json:"timePartiallyAccessibleUntil"` + TripMeter1 int `json:"tripMeter1"` + TripMeter1Timestamp string `json:"tripMeter1Timestamp"` + TripMeter2 int `json:"tripMeter2"` + TripMeter2Timestamp string `json:"tripMeter2Timestamp"` } // Volvo is an api.Vehicle implementation for Volvo cars @@ -220,6 +220,18 @@ func (v *Volvo) Range() (int64, error) { return 0, err } +var _ api.VehicleOdometer = (*Volvo)(nil) + +// VehicleOdometer implements the api.VehicleOdometer interface +func (v *Volvo) Odometer() (float64, error) { + res, err := v.statusG() + if res, ok := res.(volvoStatus); err == nil && ok { + return float64(res.Odometer), nil + } + + return 0, err +} + var _ api.VehicleFinishTimer = (*Volvo)(nil) // FinishTime implements the VehicleFinishTimer interface