Porsche: add apis and fix model selection (#1798)
- Make Odomoter, Climater, ChargeState, FinishTime APIs actually usable, they weren't exposed before - Combine Provider and ProviderEmobility into one, as they complement each other instead of being different APIs for the same purpose - Fix SoC not working since 27th October. The Emobility API only works if the Status API is called before. Otherwise the Emobility API returns HTTP 502 - Call status api only once before calling the capabilities Emobility API, as the 502 error only happens for that one call which we only need once per vehicle anyway - Return proper `api.ErrNotAvailable` errors for vehicles not supporting a specific API - Only do one call at best for every API. Status API is used if Emobility API is called but fails
This commit is contained in:
parent
b55b2643a3
commit
e1f8a50062
5 changed files with 322 additions and 350 deletions
|
|
@ -11,7 +11,11 @@ import (
|
|||
|
||||
type porscheProvider interface {
|
||||
api.Battery
|
||||
api.ChargeState
|
||||
api.VehicleRange
|
||||
api.VehicleClimater
|
||||
api.VehicleFinishTimer
|
||||
api.VehicleOdometer
|
||||
}
|
||||
|
||||
// Porsche is an api.Vehicle implementation for Porsche cars
|
||||
|
|
@ -47,17 +51,12 @@ func NewPorscheFromConfig(other map[string]interface{}) (api.Vehicle, error) {
|
|||
return nil, fmt.Errorf("login failed: %w", err)
|
||||
}
|
||||
|
||||
vehicle, err := identity.FindVehicle(accessTokens, cc.VIN)
|
||||
vin, err := identity.FindVehicle(accessTokens, cc.VIN)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var provider porscheProvider
|
||||
if vehicle.EmobilityVehicle {
|
||||
provider = porsche.NewEMobilityProvider(log, identity, accessTokens.EmobilityToken, vehicle.VIN, cc.Cache)
|
||||
} else {
|
||||
provider = porsche.NewProvider(log, identity, accessTokens.Token, vehicle.VIN, cc.Cache)
|
||||
}
|
||||
provider := porsche.NewProvider(log, identity, accessTokens, vin, cc.Cache)
|
||||
|
||||
v := &Porsche{
|
||||
embed: &cc.embed,
|
||||
|
|
|
|||
|
|
@ -7,9 +7,9 @@ import (
|
|||
"net/http/cookiejar"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/evcc-io/evcc/util"
|
||||
"github.com/evcc-io/evcc/util/oauth"
|
||||
"github.com/evcc-io/evcc/util/request"
|
||||
cv "github.com/nirasan/go-oauth-pkce-code-verifier"
|
||||
"golang.org/x/net/publicsuffix"
|
||||
|
|
@ -21,13 +21,6 @@ const (
|
|||
EmobilityClientID = "NJOxLv4QQNrpZnYQbb7mCvdiMxQWkHDq"
|
||||
)
|
||||
|
||||
type tokenResponse struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
IDToken string `json:"id_token"`
|
||||
TokenType string `json:"token_type"`
|
||||
ExpiresIn int `json:"expires_in"`
|
||||
}
|
||||
|
||||
type AccessTokens struct {
|
||||
Token, EmobilityToken oauth2.Token
|
||||
}
|
||||
|
|
@ -103,27 +96,24 @@ func (v *Identity) Login() (AccessTokens, error) {
|
|||
resp.Body.Close()
|
||||
|
||||
// get the token for the generic API
|
||||
var pr tokenResponse
|
||||
if pr, err = v.fetchToken(false); err != nil {
|
||||
var token oauth.Token
|
||||
token, err = v.fetchToken(false)
|
||||
if err != nil {
|
||||
return accessTokens, err
|
||||
}
|
||||
accessTokens.Token = (oauth2.Token)(token)
|
||||
|
||||
accessTokens.Token.AccessToken = pr.AccessToken
|
||||
accessTokens.Token.Expiry = time.Now().Add(time.Duration(pr.ExpiresIn) * time.Second)
|
||||
|
||||
if pr, err = v.fetchToken(true); err != nil {
|
||||
// we don't need to return this error, because we simply won't use the emobility API in this case
|
||||
return accessTokens, nil
|
||||
token, err = v.fetchToken(true)
|
||||
if err != nil {
|
||||
return accessTokens, err
|
||||
}
|
||||
accessTokens.EmobilityToken = (oauth2.Token)(token)
|
||||
|
||||
accessTokens.EmobilityToken.AccessToken = pr.AccessToken
|
||||
accessTokens.EmobilityToken.Expiry = time.Now().Add(time.Duration(pr.ExpiresIn) * time.Second)
|
||||
|
||||
return accessTokens, nil
|
||||
return accessTokens, err
|
||||
}
|
||||
|
||||
func (v *Identity) fetchToken(emobility bool) (tokenResponse, error) {
|
||||
var pr tokenResponse
|
||||
func (v *Identity) fetchToken(emobility bool) (oauth.Token, error) {
|
||||
var pr oauth.Token
|
||||
|
||||
actualClientID := ClientID
|
||||
redirectURI := "https://my.porsche.com/core/de/de_DE/"
|
||||
|
|
@ -187,72 +177,81 @@ func (v *Identity) fetchToken(emobility bool) (tokenResponse, error) {
|
|||
err = v.DoJSON(req, &pr)
|
||||
}
|
||||
|
||||
if pr.AccessToken == "" || pr.ExpiresIn == 0 {
|
||||
if pr.AccessToken == "" {
|
||||
return pr, errors.New("could not obtain token")
|
||||
}
|
||||
|
||||
return pr, err
|
||||
}
|
||||
|
||||
type Vehicle struct {
|
||||
VIN string
|
||||
EmobilityVehicle bool
|
||||
}
|
||||
|
||||
type VehicleResponse struct {
|
||||
VIN string
|
||||
ModelDescription string
|
||||
Pictures []struct {
|
||||
URL string
|
||||
View string
|
||||
Size int
|
||||
Width int
|
||||
Height int
|
||||
Transparent bool
|
||||
}
|
||||
}
|
||||
|
||||
func (v *Identity) FindVehicle(accessTokens AccessTokens, vin string) (Vehicle, error) {
|
||||
uri := "https://api.porsche.com/core/api/v3/de/de_DE/vehicles"
|
||||
req, err := request.New(http.MethodGet, uri, nil, map[string]string{
|
||||
func (v *Identity) FindVehicle(accessTokens AccessTokens, vin string) (string, error) {
|
||||
vehiclesURL := "https://api.porsche.com/core/api/v3/de/de_DE/vehicles"
|
||||
req, err := request.New(http.MethodGet, vehiclesURL, nil, map[string]string{
|
||||
"Authorization": fmt.Sprintf("Bearer %s", accessTokens.Token.AccessToken),
|
||||
"apikey": EmobilityClientID,
|
||||
"apikey": ClientID,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var vehicles []VehicleResponse
|
||||
if err == nil {
|
||||
err = v.DoJSON(req, &vehicles)
|
||||
if err = v.DoJSON(req, &vehicles); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var foundVehicle VehicleResponse
|
||||
var foundEmobilityVehicle bool
|
||||
|
||||
if err == nil {
|
||||
if vin == "" && len(vehicles) == 1 {
|
||||
foundVehicle = vehicles[0]
|
||||
} else {
|
||||
for _, vehicleItem := range vehicles {
|
||||
if vehicleItem.VIN == strings.ToUpper(vin) {
|
||||
foundVehicle = vehicleItem
|
||||
}
|
||||
if vin == "" && len(vehicles) == 1 {
|
||||
foundVehicle = vehicles[0]
|
||||
} else {
|
||||
for _, vehicleItem := range vehicles {
|
||||
if vehicleItem.VIN == strings.ToUpper(vin) {
|
||||
foundVehicle = vehicleItem
|
||||
}
|
||||
}
|
||||
|
||||
if foundVehicle.VIN != "" {
|
||||
v.log.DEBUG.Printf("found vehicle: %v", foundVehicle.VIN)
|
||||
|
||||
if accessTokens.EmobilityToken.AccessToken != "" {
|
||||
foundEmobilityVehicle = true
|
||||
}
|
||||
} else {
|
||||
err = errors.New("vin not found")
|
||||
}
|
||||
}
|
||||
if foundVehicle.VIN == "" {
|
||||
return "", errors.New("vin not found")
|
||||
}
|
||||
|
||||
vehicle := Vehicle{
|
||||
VIN: foundVehicle.VIN,
|
||||
EmobilityVehicle: foundEmobilityVehicle,
|
||||
v.log.DEBUG.Printf("found vehicle: %v", foundVehicle.VIN)
|
||||
|
||||
// check if vehicle is paired
|
||||
uri := fmt.Sprintf("%s/%s/pairing", vehiclesURL, foundVehicle.VIN)
|
||||
req, err = request.New(http.MethodGet, uri, nil, map[string]string{
|
||||
"Authorization": fmt.Sprintf("Bearer %s", accessTokens.Token.AccessToken),
|
||||
"apikey": ClientID,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return vehicle, err
|
||||
var pairing VehiclePairingResponse
|
||||
if err = v.DoJSON(req, &pairing); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if pairing.Status != "PAIRINGCOMPLETE" {
|
||||
return "", errors.New("vehicle is not paired with the My Porsche account")
|
||||
}
|
||||
|
||||
// now check if we get any response at all for a status request
|
||||
// there are PHEV which do not provide any data, even thought they are PHEV
|
||||
uri = fmt.Sprintf("https://api.porsche.com/vehicle-data/de/de_DE/status/%s", foundVehicle.VIN)
|
||||
req, err = request.New(http.MethodGet, uri, nil, map[string]string{
|
||||
"Authorization": fmt.Sprintf("Bearer %s", accessTokens.Token.AccessToken),
|
||||
"apikey": ClientID,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if _, err = v.DoBody(req); err != nil {
|
||||
return "", errors.New("vehicle is not capable of providing data")
|
||||
}
|
||||
|
||||
return foundVehicle.VIN, err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package porsche
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
|
@ -9,92 +10,135 @@ import (
|
|||
"github.com/evcc-io/evcc/provider"
|
||||
"github.com/evcc-io/evcc/util"
|
||||
"github.com/evcc-io/evcc/util/request"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
type StatusResponse struct {
|
||||
CarControlData struct {
|
||||
BatteryLevel struct {
|
||||
Unit string
|
||||
Value float64
|
||||
}
|
||||
Mileage struct {
|
||||
Unit string
|
||||
Value float64
|
||||
}
|
||||
RemainingRanges struct {
|
||||
ElectricalRange struct {
|
||||
Distance struct {
|
||||
Unit string
|
||||
Value float64
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Provider is an api.Vehicle implementation for Porsche PHEV cars
|
||||
type Provider struct {
|
||||
log *util.Logger
|
||||
*request.Helper
|
||||
token oauth2.Token
|
||||
identity *Identity
|
||||
statusG func() (interface{}, error)
|
||||
accessTokens AccessTokens
|
||||
identity *Identity
|
||||
carModel string
|
||||
statusG func() (interface{}, error)
|
||||
statusEmobilityG func() (interface{}, error)
|
||||
}
|
||||
|
||||
// NewProvider creates a new vehicle
|
||||
func NewProvider(log *util.Logger, identity *Identity, token oauth2.Token, vin string, cache time.Duration) *Provider {
|
||||
func NewProvider(log *util.Logger, identity *Identity, accessTokens AccessTokens, vin string, cache time.Duration) *Provider {
|
||||
impl := &Provider{
|
||||
log: log,
|
||||
Helper: request.NewHelper(log),
|
||||
token: token,
|
||||
identity: identity,
|
||||
log: log,
|
||||
Helper: request.NewHelper(log),
|
||||
accessTokens: accessTokens,
|
||||
identity: identity,
|
||||
}
|
||||
|
||||
impl.Client.Jar = identity.Client.Jar
|
||||
|
||||
impl.statusG = provider.NewCached(func() (interface{}, error) {
|
||||
return impl.status(vin)
|
||||
}, cache).InterfaceGetter()
|
||||
|
||||
impl.statusEmobilityG = provider.NewCached(func() (interface{}, error) {
|
||||
return impl.statusEmobility(vin)
|
||||
}, cache).InterfaceGetter()
|
||||
|
||||
return impl
|
||||
}
|
||||
|
||||
func (v *Provider) request(uri string) (*http.Request, error) {
|
||||
if v.token.AccessToken == "" || time.Since(v.token.Expiry) > 0 {
|
||||
func (v *Provider) request(emobility bool, uri string) (*http.Request, error) {
|
||||
token := v.accessTokens.Token
|
||||
apiKey := ClientID
|
||||
if emobility {
|
||||
token = v.accessTokens.EmobilityToken
|
||||
apiKey = EmobilityClientID
|
||||
}
|
||||
|
||||
if token.AccessToken == "" || time.Since(token.Expiry) > 0 {
|
||||
accessTokens, err := v.identity.Login()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
v.token = accessTokens.Token
|
||||
v.accessTokens = accessTokens
|
||||
}
|
||||
|
||||
req, err := request.New(http.MethodGet, uri, nil, map[string]string{
|
||||
"Authorization": fmt.Sprintf("Bearer %s", v.token.AccessToken),
|
||||
"Authorization": fmt.Sprintf("Bearer %s", token.AccessToken),
|
||||
"apikey": apiKey,
|
||||
})
|
||||
|
||||
return req, err
|
||||
}
|
||||
|
||||
// Status implements the vehicle status response
|
||||
func (v *Provider) status(vin string) (interface{}, error) {
|
||||
uri := fmt.Sprintf("https://connect-portal.porsche.com/core/api/v3/de/de_DE/vehicles/%s", vin)
|
||||
req, err := v.request(uri)
|
||||
func (v *Provider) status(vin string) (StatusResponse, error) {
|
||||
var res StatusResponse
|
||||
|
||||
uri := fmt.Sprintf("https://api.porsche.com/vehicle-data/de/de_DE/status/%s", vin)
|
||||
req, err := v.request(false, uri)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
return res, err
|
||||
}
|
||||
|
||||
var pr StatusResponse
|
||||
err = v.DoJSON(req, &pr)
|
||||
err = v.DoJSON(req, &res)
|
||||
|
||||
return pr, err
|
||||
return res, err
|
||||
}
|
||||
|
||||
// Status implements the vehicle status response
|
||||
func (v *Provider) statusEmobility(vin string) (EmobilityResponse, error) {
|
||||
var res EmobilityResponse
|
||||
|
||||
if v.carModel == "" {
|
||||
// Note: As of 27.10.21 the capabilities API needs to be called AFTER a
|
||||
// call to status() as it otherwise returns an HTTP 502 error.
|
||||
// The reason is unknown, even when tested with 100% identical Headers.
|
||||
// It seems to be a new backend related issue.
|
||||
|
||||
if _, err := v.status(vin); err != nil {
|
||||
return res, err
|
||||
}
|
||||
|
||||
uri := fmt.Sprintf("https://api.porsche.com/e-mobility/vcs/capabilities/%s", vin)
|
||||
|
||||
req, err := v.request(true, uri)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
|
||||
var cr CapabilitiesResponse
|
||||
err = v.DoJSON(req, &cr)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
v.carModel = cr.CarModel
|
||||
}
|
||||
|
||||
uri := fmt.Sprintf("https://api.porsche.com/e-mobility/de/de_DE/%s/%s?timezone=Europe/Berlin", v.carModel, vin)
|
||||
req, err := v.request(true, uri)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
|
||||
err = v.DoJSON(req, &res)
|
||||
if err != nil && res.PcckErrorMessage != "" {
|
||||
err = errors.New(res.PcckErrorMessage)
|
||||
}
|
||||
|
||||
return res, err
|
||||
}
|
||||
|
||||
var _ api.Battery = (*Provider)(nil)
|
||||
|
||||
// SoC implements the api.Vehicle interface
|
||||
func (v *Provider) SoC() (float64, error) {
|
||||
res, err := v.statusG()
|
||||
res, err := v.statusEmobilityG()
|
||||
if res, ok := res.(EmobilityResponse); err == nil && ok {
|
||||
return float64(res.BatteryChargeStatus.StateOfChargeInPercentage), nil
|
||||
}
|
||||
|
||||
res, err = v.statusG()
|
||||
if res, ok := res.(StatusResponse); err == nil && ok {
|
||||
return res.CarControlData.BatteryLevel.Value, nil
|
||||
return res.BatteryLevel.Value, nil
|
||||
}
|
||||
|
||||
return 0, err
|
||||
|
|
@ -104,21 +148,83 @@ var _ api.VehicleRange = (*Provider)(nil)
|
|||
|
||||
// Range implements the api.VehicleRange interface
|
||||
func (v *Provider) Range() (int64, error) {
|
||||
res, err := v.statusG()
|
||||
res, err := v.statusEmobilityG()
|
||||
if res, ok := res.(EmobilityResponse); err == nil && ok {
|
||||
return int64(res.BatteryChargeStatus.RemainingERange.ValueInKilometers), nil
|
||||
}
|
||||
|
||||
res, err = v.statusG()
|
||||
if res, ok := res.(StatusResponse); err == nil && ok {
|
||||
return int64(res.CarControlData.RemainingRanges.ElectricalRange.Distance.Value), nil
|
||||
return int64(res.RemainingRanges.ElectricalRange.Distance.Value), nil
|
||||
}
|
||||
|
||||
return 0, err
|
||||
}
|
||||
|
||||
var _ api.VehicleFinishTimer = (*Provider)(nil)
|
||||
|
||||
// FinishTime implements the api.VehicleFinishTimer interface
|
||||
func (v *Provider) FinishTime() (time.Time, error) {
|
||||
res, err := v.statusEmobilityG()
|
||||
if res, ok := res.(EmobilityResponse); err == nil && ok {
|
||||
return time.Now().Add(time.Duration(res.BatteryChargeStatus.RemainingChargeTimeUntil100PercentInMinutes) * time.Minute), err
|
||||
}
|
||||
|
||||
return time.Time{}, err
|
||||
}
|
||||
|
||||
var _ api.ChargeState = (*Provider)(nil)
|
||||
|
||||
// Status implements the api.ChargeState interface
|
||||
func (v *Provider) Status() (api.ChargeStatus, error) {
|
||||
res, err := v.statusEmobilityG()
|
||||
if res, ok := res.(EmobilityResponse); err == nil && ok {
|
||||
switch res.BatteryChargeStatus.PlugState {
|
||||
case "DISCONNECTED":
|
||||
return api.StatusA, nil
|
||||
case "CONNECTED":
|
||||
// ignore if the car is connected to a DC charging station
|
||||
if res.BatteryChargeStatus.ChargingInDCMode {
|
||||
return api.StatusA, nil
|
||||
}
|
||||
switch res.BatteryChargeStatus.ChargingState {
|
||||
case "ERROR":
|
||||
return api.StatusF, nil
|
||||
case "OFF", "COMPLETED":
|
||||
return api.StatusB, nil
|
||||
case "ON":
|
||||
return api.StatusC, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return api.StatusNone, err
|
||||
}
|
||||
|
||||
var _ api.VehicleClimater = (*Provider)(nil)
|
||||
|
||||
// Climater implements the api.VehicleClimater interface
|
||||
func (v *Provider) Climater() (active bool, outsideTemp float64, targetTemp float64, err error) {
|
||||
res, err := v.statusEmobilityG()
|
||||
if res, ok := res.(EmobilityResponse); err == nil && ok {
|
||||
switch res.DirectClimatisation.ClimatisationState {
|
||||
case "OFF":
|
||||
return false, 20, 20, nil
|
||||
case "ON":
|
||||
return true, 20, 20, nil
|
||||
}
|
||||
}
|
||||
|
||||
return active, outsideTemp, targetTemp, 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 res.Mileage.Value, nil
|
||||
}
|
||||
|
||||
return 0, err
|
||||
|
|
|
|||
|
|
@ -1,227 +0,0 @@
|
|||
package porsche
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/evcc-io/evcc/api"
|
||||
"github.com/evcc-io/evcc/provider"
|
||||
"github.com/evcc-io/evcc/util"
|
||||
"github.com/evcc-io/evcc/util/request"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
type CapabilitiesResponse struct {
|
||||
DisplayParkingBrake bool
|
||||
NeedsSPIN bool
|
||||
HasRDK bool
|
||||
EngineType string
|
||||
CarModel string
|
||||
OnlineRemoteUpdateStatus struct {
|
||||
EditableByUser bool
|
||||
Active bool
|
||||
}
|
||||
HeatingCapabilities struct {
|
||||
FrontSeatHeatingAvailable bool
|
||||
RearSeatHeatingAvailable bool
|
||||
}
|
||||
SteeringWheelPosition string
|
||||
HasHonkAndFlash bool
|
||||
}
|
||||
|
||||
type EmobilityResponse struct {
|
||||
BatteryChargeStatus struct {
|
||||
ChargeRate struct {
|
||||
Unit string
|
||||
Value float64
|
||||
ValueInKmPerHour int64
|
||||
}
|
||||
ChargingInDCMode bool
|
||||
ChargingMode string
|
||||
ChargingPower float64
|
||||
ChargingReason string
|
||||
ChargingState string
|
||||
ChargingTargetDateTime string
|
||||
ExternalPowerSupplyState string
|
||||
PlugState string
|
||||
RemainingChargeTimeUntil100PercentInMinutes int64
|
||||
StateOfChargeInPercentage int64
|
||||
RemainingERange struct {
|
||||
OriginalUnit string
|
||||
OriginalValue int64
|
||||
Unit string
|
||||
Value int64
|
||||
ValueInKilometers int64
|
||||
}
|
||||
}
|
||||
ChargingStatus string
|
||||
DirectCharge struct {
|
||||
Disabled bool
|
||||
IsActive bool
|
||||
}
|
||||
DirectClimatisation struct {
|
||||
ClimatisationState string
|
||||
RemainingClimatisationTime int64
|
||||
}
|
||||
PcckErrorMessage string
|
||||
}
|
||||
|
||||
// EMobilityProvider is an api.Vehicle implementation for Porsche Taycan cars
|
||||
type EMobilityProvider struct {
|
||||
log *util.Logger
|
||||
*request.Helper
|
||||
token oauth2.Token
|
||||
identity *Identity
|
||||
carModel string
|
||||
statusG func() (interface{}, error)
|
||||
}
|
||||
|
||||
// NewEMobilityProvider creates a new vehicle
|
||||
func NewEMobilityProvider(log *util.Logger, identity *Identity, token oauth2.Token, vin string, cache time.Duration) *EMobilityProvider {
|
||||
impl := &EMobilityProvider{
|
||||
log: log,
|
||||
token: token,
|
||||
Helper: request.NewHelper(log),
|
||||
identity: identity,
|
||||
}
|
||||
|
||||
impl.statusG = provider.NewCached(func() (interface{}, error) {
|
||||
return impl.status(vin)
|
||||
}, cache).InterfaceGetter()
|
||||
|
||||
return impl
|
||||
}
|
||||
|
||||
func (v *EMobilityProvider) request(uri string) (*http.Request, error) {
|
||||
if v.token.AccessToken == "" || time.Since(v.token.Expiry) > 0 {
|
||||
accessTokens, err := v.identity.Login()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
v.token = accessTokens.EmobilityToken
|
||||
}
|
||||
|
||||
req, err := request.New(http.MethodGet, uri, nil, map[string]string{
|
||||
"Authorization": fmt.Sprintf("Bearer %s", v.token.AccessToken),
|
||||
"apikey": EmobilityClientID,
|
||||
})
|
||||
|
||||
return req, err
|
||||
}
|
||||
|
||||
// Status implements the vehicle status response
|
||||
func (v *EMobilityProvider) status(vin string) (interface{}, error) {
|
||||
if v.carModel == "" {
|
||||
uri := fmt.Sprintf("https://api.porsche.com/e-mobility/vcs/capabilities/%s", vin)
|
||||
req, err := v.request(uri)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
req.Header.Set("x-vrs-url-country", "de")
|
||||
req.Header.Set("x-vrs-url-language", "de_DE")
|
||||
var cr CapabilitiesResponse
|
||||
err = v.DoJSON(req, &cr)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
v.carModel = cr.CarModel
|
||||
}
|
||||
|
||||
uri := fmt.Sprintf("https://api.porsche.com/e-mobility/de/de_DE/%s/%s?timezone=Europe/Berlin", v.carModel, vin)
|
||||
req, err := v.request(uri)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
var pr EmobilityResponse
|
||||
err = v.DoJSON(req, &pr)
|
||||
|
||||
if err != nil && pr.PcckErrorMessage != "" {
|
||||
err = errors.New(pr.PcckErrorMessage)
|
||||
}
|
||||
|
||||
return pr, err
|
||||
}
|
||||
|
||||
var _ api.Battery = (*Provider)(nil)
|
||||
|
||||
// SoC implements the api.Vehicle interface
|
||||
func (v *EMobilityProvider) SoC() (float64, error) {
|
||||
res, err := v.statusG()
|
||||
if res, ok := res.(EmobilityResponse); err == nil && ok {
|
||||
return float64(res.BatteryChargeStatus.StateOfChargeInPercentage), nil
|
||||
}
|
||||
|
||||
return 0, err
|
||||
}
|
||||
|
||||
var _ api.VehicleRange = (*Provider)(nil)
|
||||
|
||||
// Range implements the api.VehicleRange interface
|
||||
func (v *EMobilityProvider) Range() (int64, error) {
|
||||
res, err := v.statusG()
|
||||
if res, ok := res.(EmobilityResponse); err == nil && ok {
|
||||
return int64(res.BatteryChargeStatus.RemainingERange.ValueInKilometers), nil
|
||||
}
|
||||
|
||||
return 0, err
|
||||
}
|
||||
|
||||
var _ api.VehicleFinishTimer = (*EMobilityProvider)(nil)
|
||||
|
||||
// FinishTime implements the api.VehicleFinishTimer interface
|
||||
func (v *EMobilityProvider) FinishTime() (time.Time, error) {
|
||||
res, err := v.statusG()
|
||||
if res, ok := res.(*EmobilityResponse); err == nil && ok {
|
||||
t := time.Now()
|
||||
return t.Add(time.Duration(res.BatteryChargeStatus.RemainingChargeTimeUntil100PercentInMinutes) * time.Minute), err
|
||||
}
|
||||
|
||||
return time.Time{}, err
|
||||
}
|
||||
|
||||
var _ api.ChargeState = (*EMobilityProvider)(nil)
|
||||
|
||||
// Status implements the api.ChargeState interface
|
||||
func (v *EMobilityProvider) Status() (api.ChargeStatus, error) {
|
||||
res, err := v.statusG()
|
||||
if res, ok := res.(EmobilityResponse); err == nil && ok {
|
||||
switch res.BatteryChargeStatus.PlugState {
|
||||
case "DISCONNECTED":
|
||||
return api.StatusA, nil
|
||||
case "CONNECTED":
|
||||
// ignore if the car is connected to a DC charging station
|
||||
if res.BatteryChargeStatus.ChargingInDCMode {
|
||||
return api.StatusA, nil
|
||||
}
|
||||
switch res.BatteryChargeStatus.ChargingState {
|
||||
case "OFF", "COMPLETED":
|
||||
return api.StatusB, nil
|
||||
case "ON":
|
||||
return api.StatusC, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return api.StatusNone, err
|
||||
}
|
||||
|
||||
var _ api.VehicleClimater = (*EMobilityProvider)(nil)
|
||||
|
||||
// Climater implements the api.VehicleClimater interface
|
||||
func (v *EMobilityProvider) Climater() (active bool, outsideTemp float64, targetTemp float64, err error) {
|
||||
res, err := v.statusG()
|
||||
if res, ok := res.(EmobilityResponse); err == nil && ok {
|
||||
switch res.DirectClimatisation.ClimatisationState {
|
||||
case "OFF":
|
||||
return false, 0, 0, nil
|
||||
case "ON":
|
||||
return true, 0, 0, nil
|
||||
}
|
||||
}
|
||||
|
||||
return active, outsideTemp, targetTemp, err
|
||||
}
|
||||
95
vehicle/porsche/types.go
Normal file
95
vehicle/porsche/types.go
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
package porsche
|
||||
|
||||
type VehicleResponse struct {
|
||||
VIN string
|
||||
ModelDescription string
|
||||
Pictures []struct {
|
||||
URL string
|
||||
View string
|
||||
Size int
|
||||
Width int
|
||||
Height int
|
||||
Transparent bool
|
||||
}
|
||||
}
|
||||
|
||||
type VehiclePairingResponse struct {
|
||||
VIN string
|
||||
PairingCode string
|
||||
Status string
|
||||
CanSendPairingCode bool
|
||||
}
|
||||
|
||||
type StatusResponse struct {
|
||||
BatteryLevel struct {
|
||||
Unit string
|
||||
Value float64
|
||||
}
|
||||
Mileage struct {
|
||||
Unit string
|
||||
Value float64
|
||||
}
|
||||
RemainingRanges struct {
|
||||
ElectricalRange struct {
|
||||
Distance struct {
|
||||
Unit string
|
||||
Value float64
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type CapabilitiesResponse struct {
|
||||
DisplayParkingBrake bool
|
||||
NeedsSPIN bool
|
||||
HasRDK bool
|
||||
EngineType string
|
||||
CarModel string
|
||||
OnlineRemoteUpdateStatus struct {
|
||||
EditableByUser bool
|
||||
Active bool
|
||||
}
|
||||
HeatingCapabilities struct {
|
||||
FrontSeatHeatingAvailable bool
|
||||
RearSeatHeatingAvailable bool
|
||||
}
|
||||
SteeringWheelPosition string
|
||||
HasHonkAndFlash bool
|
||||
}
|
||||
|
||||
type EmobilityResponse struct {
|
||||
BatteryChargeStatus struct {
|
||||
ChargeRate struct {
|
||||
Unit string
|
||||
Value float64
|
||||
ValueInKmPerHour int64
|
||||
}
|
||||
ChargingInDCMode bool
|
||||
ChargingMode string
|
||||
ChargingPower float64
|
||||
ChargingReason string
|
||||
ChargingState string
|
||||
ChargingTargetDateTime string
|
||||
ExternalPowerSupplyState string
|
||||
PlugState string
|
||||
RemainingChargeTimeUntil100PercentInMinutes int64
|
||||
StateOfChargeInPercentage int64
|
||||
RemainingERange struct {
|
||||
OriginalUnit string
|
||||
OriginalValue int64
|
||||
Unit string
|
||||
Value int64
|
||||
ValueInKilometers int64
|
||||
}
|
||||
}
|
||||
ChargingStatus string
|
||||
DirectCharge struct {
|
||||
Disabled bool
|
||||
IsActive bool
|
||||
}
|
||||
DirectClimatisation struct {
|
||||
ClimatisationState string
|
||||
RemainingClimatisationTime int64
|
||||
}
|
||||
PcckErrorMessage string
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue