Add Porsche Mobile API support (#2439)
This commit is contained in:
parent
12f984bb06
commit
62680b6f20
6 changed files with 364 additions and 28 deletions
|
|
@ -48,8 +48,16 @@ func NewPorscheFromConfig(other map[string]interface{}) (api.Vehicle, error) {
|
|||
}
|
||||
|
||||
api := porsche.NewAPI(log, identity.DefaultSource)
|
||||
mobile := porsche.NewMobileAPI(log, identity.MobileSource)
|
||||
|
||||
cc.VIN, err = ensureVehicle(cc.VIN, func() ([]string, error) {
|
||||
mobileVehicles, err := mobile.Vehicles()
|
||||
if err == nil {
|
||||
return funk.Map(mobileVehicles, func(v porsche.StatusResponseMobile) string {
|
||||
return v.VIN
|
||||
}).([]string), err
|
||||
}
|
||||
|
||||
vehicles, err := api.Vehicles()
|
||||
return funk.Map(vehicles, func(v porsche.Vehicle) string {
|
||||
return v.VIN
|
||||
|
|
@ -61,13 +69,22 @@ func NewPorscheFromConfig(other map[string]interface{}) (api.Vehicle, error) {
|
|||
}
|
||||
|
||||
// check if vehicle is paired
|
||||
if res, err := api.PairingStatus(cc.VIN); err == nil && res.Status != porsche.PairingComplete {
|
||||
return nil, errors.New("vehicle is not paired with the My Porsche account")
|
||||
}
|
||||
// if res, err := api.PairingStatus(cc.VIN); err == nil && res.Status != porsche.PairingComplete {
|
||||
// return nil, errors.New("vehicle is not paired with the My Porsche account")
|
||||
// }
|
||||
|
||||
// check if vehicle provides status:
|
||||
// some PHEVs do not provide any data
|
||||
if _, err := api.Status(cc.VIN); err != nil {
|
||||
statusAvailable := false
|
||||
if _, err := mobile.Status(cc.VIN); err == nil {
|
||||
statusAvailable = true
|
||||
}
|
||||
|
||||
if _, err := api.Status(cc.VIN); err == nil {
|
||||
statusAvailable = true
|
||||
}
|
||||
|
||||
if !statusAvailable {
|
||||
return nil, errors.New("vehicle is not capable of providing data")
|
||||
}
|
||||
|
||||
|
|
@ -77,17 +94,14 @@ func NewPorscheFromConfig(other map[string]interface{}) (api.Vehicle, error) {
|
|||
// 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 := api.Status(cc.VIN); err != nil {
|
||||
return nil, err
|
||||
var emobility *porsche.EmobilityAPI
|
||||
var capabilities porsche.CapabilitiesResponse
|
||||
if _, err := api.Status(cc.VIN); err == nil {
|
||||
emobility = porsche.NewEmobilityAPI(log, identity.EmobilitySource)
|
||||
capabilities, _ = emobility.Capabilities(cc.VIN)
|
||||
}
|
||||
|
||||
emobility := porsche.NewEmobilityAPI(log, identity.EmobilitySource)
|
||||
capabilities, err := emobility.Capabilities(cc.VIN)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
provider := porsche.NewProvider(log, api, emobility, cc.VIN, capabilities.CarModel, cc.Cache)
|
||||
provider := porsche.NewProvider(log, api, emobility, mobile, cc.VIN, capabilities.CarModel, cc.Cache)
|
||||
|
||||
v := &Porsche{
|
||||
embed: &cc.embed,
|
||||
|
|
|
|||
|
|
@ -32,7 +32,8 @@ func NewAPI(log *util.Logger, identity oauth2.TokenSource) *API {
|
|||
Base: v.Client.Transport,
|
||||
},
|
||||
Decorator: transport.DecorateHeaders(map[string]string{
|
||||
"apikey": OAuth2Config.ClientID,
|
||||
"apikey": OAuth2Config.ClientID,
|
||||
"x-client-id": "52064df8-6daa-46f7-bc9e-e3232622ab26",
|
||||
}),
|
||||
}
|
||||
|
||||
|
|
|
|||
55
vehicle/porsche/api_mobile.go
Normal file
55
vehicle/porsche/api_mobile.go
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
package porsche
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/evcc-io/evcc/util"
|
||||
"github.com/evcc-io/evcc/util/request"
|
||||
"github.com/evcc-io/evcc/util/transport"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
const (
|
||||
MobileApiURI = "https://api.ppa.porsche.com"
|
||||
)
|
||||
|
||||
// API is an api.Vehicle implementation for Porsche PHEV cars
|
||||
type MobileAPI struct {
|
||||
*request.Helper
|
||||
}
|
||||
|
||||
// NewAPI creates a new vehicle
|
||||
func NewMobileAPI(log *util.Logger, identity oauth2.TokenSource) *MobileAPI {
|
||||
v := &MobileAPI{
|
||||
Helper: request.NewHelper(log),
|
||||
}
|
||||
|
||||
v.Client.Transport = &transport.Decorator{
|
||||
Base: &oauth2.Transport{
|
||||
Source: identity,
|
||||
Base: v.Client.Transport,
|
||||
},
|
||||
Decorator: transport.DecorateHeaders(map[string]string{
|
||||
"apikey": OAuth2Config.ClientID,
|
||||
"x-client-id": "52064df8-6daa-46f7-bc9e-e3232622ab26",
|
||||
}),
|
||||
}
|
||||
|
||||
return v
|
||||
}
|
||||
|
||||
// Vehicles implements the vehicle list response
|
||||
func (v *MobileAPI) Vehicles() ([]StatusResponseMobile, error) {
|
||||
var res []StatusResponseMobile
|
||||
uri := fmt.Sprintf("%s/app/connect/v1/vehicles", MobileApiURI)
|
||||
err := v.GetJSON(uri, &res)
|
||||
return res, err
|
||||
}
|
||||
|
||||
// Status implements the vehicle status response
|
||||
func (v *MobileAPI) Status(vin string) (StatusResponseMobile, error) {
|
||||
var res StatusResponseMobile
|
||||
uri := fmt.Sprintf("%s/app/connect/v1/vehicles/%s?mf=*", MobileApiURI, vin)
|
||||
err := v.GetJSON(uri, &res)
|
||||
return res, err
|
||||
}
|
||||
|
|
@ -37,16 +37,23 @@ var (
|
|||
ClientID: "NJOxLv4QQNrpZnYQbb7mCvdiMxQWkHDq",
|
||||
RedirectURL: "https://my.porsche.com/myservices/auth/auth.html",
|
||||
Endpoint: OAuth2Config.Endpoint,
|
||||
Scopes: OAuth2Config.Scopes,
|
||||
Scopes: []string{"openid"},
|
||||
}
|
||||
|
||||
MobileOAuth2Config = &oauth2.Config{
|
||||
ClientID: "L20OiZ0kBgWt958NWbuCB8gb970y6V6U",
|
||||
RedirectURL: "One-Product-App://porsche-id/oauth2redirect",
|
||||
Endpoint: OAuth2Config.Endpoint,
|
||||
Scopes: []string{"openid", "magiclink", "mbb"},
|
||||
}
|
||||
)
|
||||
|
||||
// Identity is the Porsche Identity client
|
||||
type Identity struct {
|
||||
*request.Helper
|
||||
user, password string
|
||||
defaultToken, emobilityToken *oauth2.Token
|
||||
DefaultSource, EmobilitySource oauth2.TokenSource
|
||||
user, password string
|
||||
defaultToken, emobilityToken, mobileToken *oauth2.Token
|
||||
DefaultSource, EmobilitySource, MobileSource oauth2.TokenSource
|
||||
}
|
||||
|
||||
// NewIdentity creates Porsche identity
|
||||
|
|
@ -66,6 +73,7 @@ func (v *Identity) Login() error {
|
|||
if err == nil {
|
||||
v.DefaultSource = oauth.RefreshTokenSource(v.defaultToken, v)
|
||||
v.EmobilitySource = oauth.RefreshTokenSource(v.emobilityToken, &emobilityAdapter{v})
|
||||
v.MobileSource = oauth.RefreshTokenSource(v.mobileToken, &mobileAdapter{v})
|
||||
}
|
||||
|
||||
return err
|
||||
|
|
@ -137,6 +145,10 @@ func (v *Identity) RefreshToken(_ *oauth2.Token) (*oauth2.Token, error) {
|
|||
if token, err = v.fetchToken(EmobilityOAuth2Config); err == nil {
|
||||
v.emobilityToken = token
|
||||
}
|
||||
|
||||
if token, err = v.fetchToken(MobileOAuth2Config); err == nil {
|
||||
v.mobileToken = token
|
||||
}
|
||||
}
|
||||
|
||||
return v.defaultToken, err
|
||||
|
|
@ -155,13 +167,24 @@ func (v *Identity) fetchToken(oc *oauth2.Config) (*oauth2.Token, error) {
|
|||
oauth2.SetAuthURLParam("locale", "de_DE"),
|
||||
)
|
||||
|
||||
v.Client.CheckRedirect = func(req *http.Request, via []*http.Request) error {
|
||||
if req.URL.Scheme != "https" {
|
||||
return http.ErrUseLastResponse
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
resp, err := v.Client.Get(uri)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp.Body.Close()
|
||||
|
||||
query, err := url.ParseQuery(resp.Request.URL.RawQuery)
|
||||
rawQuery := resp.Request.URL.RawQuery
|
||||
if strings.HasPrefix(resp.Header.Get("Location"), "One-Product-App") {
|
||||
rawQuery = strings.Replace(resp.Header.Get("Location"), "One-Product-App://porsche-id/oauth2redirect?", "", 1)
|
||||
}
|
||||
query, err := url.ParseQuery(rawQuery)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -195,3 +218,15 @@ func (v *emobilityAdapter) RefreshToken(_ *oauth2.Token) (*oauth2.Token, error)
|
|||
}
|
||||
return token, err
|
||||
}
|
||||
|
||||
type mobileAdapter struct {
|
||||
tr *Identity
|
||||
}
|
||||
|
||||
func (v *mobileAdapter) RefreshToken(_ *oauth2.Token) (*oauth2.Token, error) {
|
||||
token, err := v.tr.RefreshToken(nil)
|
||||
if err == nil {
|
||||
token = v.tr.mobileToken
|
||||
}
|
||||
return token, err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package porsche
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/evcc-io/evcc/api"
|
||||
|
|
@ -12,17 +13,25 @@ import (
|
|||
type Provider struct {
|
||||
statusG func() (interface{}, error)
|
||||
emobilityG func() (interface{}, error)
|
||||
mobileG func() (interface{}, error)
|
||||
}
|
||||
|
||||
// NewProvider creates a new vehicle
|
||||
func NewProvider(log *util.Logger, api *API, emobility *EmobilityAPI, vin, model string, cache time.Duration) *Provider {
|
||||
func NewProvider(log *util.Logger, api *API, emobility *EmobilityAPI, mobile *MobileAPI, vin string, carModel string, cache time.Duration) *Provider {
|
||||
impl := &Provider{
|
||||
statusG: provider.NewCached(func() (interface{}, error) {
|
||||
return api.Status(vin)
|
||||
}, cache).InterfaceGetter(),
|
||||
|
||||
emobilityG: provider.NewCached(func() (interface{}, error) {
|
||||
return emobility.Status(vin, model)
|
||||
if carModel != "" {
|
||||
return emobility.Status(vin, carModel)
|
||||
}
|
||||
return EmobilityResponse{}, errors.New("no car model")
|
||||
}, cache).InterfaceGetter(),
|
||||
|
||||
mobileG: provider.NewCached(func() (interface{}, error) {
|
||||
return mobile.Status(vin)
|
||||
}, cache).InterfaceGetter(),
|
||||
}
|
||||
|
||||
|
|
@ -33,7 +42,18 @@ var _ api.Battery = (*Provider)(nil)
|
|||
|
||||
// SoC implements the api.Vehicle interface
|
||||
func (v *Provider) SoC() (float64, error) {
|
||||
res, err := v.emobilityG()
|
||||
res, err := v.mobileG()
|
||||
if res, ok := res.(StatusResponseMobile); err == nil && ok {
|
||||
m, err := res.MeasurementByKey("BATTERY_LEVEL")
|
||||
if err != nil && err != api.ErrNotAvailable {
|
||||
return 0, err
|
||||
}
|
||||
if err != api.ErrNotAvailable {
|
||||
return float64(m.Value.Percent), nil
|
||||
}
|
||||
}
|
||||
|
||||
res, err = v.emobilityG()
|
||||
if res, ok := res.(EmobilityResponse); err == nil && ok {
|
||||
return float64(res.BatteryChargeStatus.StateOfChargeInPercentage), nil
|
||||
}
|
||||
|
|
@ -50,7 +70,18 @@ var _ api.VehicleRange = (*Provider)(nil)
|
|||
|
||||
// Range implements the api.VehicleRange interface
|
||||
func (v *Provider) Range() (int64, error) {
|
||||
res, err := v.emobilityG()
|
||||
res, err := v.mobileG()
|
||||
if res, ok := res.(StatusResponseMobile); err == nil && ok {
|
||||
m, err := res.MeasurementByKey("E_RANGE")
|
||||
if err != nil && err != api.ErrNotAvailable {
|
||||
return 0, err
|
||||
}
|
||||
if err != api.ErrNotAvailable {
|
||||
return int64(m.Value.Kilometers), nil
|
||||
}
|
||||
}
|
||||
|
||||
res, err = v.emobilityG()
|
||||
if res, ok := res.(EmobilityResponse); err == nil && ok {
|
||||
return res.BatteryChargeStatus.RemainingERange.ValueInKilometers, nil
|
||||
}
|
||||
|
|
@ -67,7 +98,24 @@ var _ api.VehicleFinishTimer = (*Provider)(nil)
|
|||
|
||||
// FinishTime implements the api.VehicleFinishTimer interface
|
||||
func (v *Provider) FinishTime() (time.Time, error) {
|
||||
res, err := v.emobilityG()
|
||||
res, err := v.mobileG()
|
||||
if res, ok := res.(StatusResponseMobile); err == nil && ok {
|
||||
m, err := res.MeasurementByKey("BATTERY_CHARGING_STATE")
|
||||
if err != nil && err != api.ErrNotAvailable {
|
||||
return time.Time{}, err
|
||||
}
|
||||
if err != api.ErrNotAvailable {
|
||||
if m.Value.EndsAt == "" {
|
||||
if m.Value.LastModified != "" {
|
||||
return time.Parse(time.RFC3339, m.Value.LastModified)
|
||||
}
|
||||
return time.Time{}, nil
|
||||
}
|
||||
return time.Parse(time.RFC3339, m.Value.EndsAt)
|
||||
}
|
||||
}
|
||||
|
||||
res, err = v.emobilityG()
|
||||
if res, ok := res.(EmobilityResponse); err == nil && ok {
|
||||
return time.Now().Add(time.Duration(res.BatteryChargeStatus.RemainingChargeTimeUntil100PercentInMinutes) * time.Minute), err
|
||||
}
|
||||
|
|
@ -79,7 +127,30 @@ var _ api.ChargeState = (*Provider)(nil)
|
|||
|
||||
// Status implements the api.ChargeState interface
|
||||
func (v *Provider) Status() (api.ChargeStatus, error) {
|
||||
res, err := v.emobilityG()
|
||||
res, err := v.mobileG()
|
||||
if res, ok := res.(StatusResponseMobile); err == nil && ok {
|
||||
m, err := res.MeasurementByKey("BATTERY_CHARGING_STATE")
|
||||
if err != nil && err != api.ErrNotAvailable {
|
||||
return api.StatusNone, err
|
||||
}
|
||||
if err != api.ErrNotAvailable {
|
||||
switch m.Value.Status {
|
||||
case "FAST_CHARGING", "NOT_PLUGGED", "UNKNOWN":
|
||||
return api.StatusA, nil
|
||||
case "CHARGING_COMPLETED", "CHARGING_PAUSED", "READY_TO_CHARGE", "SOC_REACHED",
|
||||
"INITIALISING", "STANDBY", "SUSPENDED", "PLUGGED_LOCKED", "PLUGGED_NOT_LOCKED":
|
||||
return api.StatusB, nil
|
||||
case "CHARGING":
|
||||
return api.StatusC, nil
|
||||
case "CHARGING_ERROR":
|
||||
return api.StatusF, nil
|
||||
default:
|
||||
return api.StatusNone, errors.New("mobile - unknown charging status: " + m.Value.Status)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
res, err = v.emobilityG()
|
||||
if res, ok := res.(EmobilityResponse); err == nil && ok {
|
||||
switch res.BatteryChargeStatus.PlugState {
|
||||
case "DISCONNECTED":
|
||||
|
|
@ -94,8 +165,10 @@ func (v *Provider) Status() (api.ChargeStatus, error) {
|
|||
return api.StatusF, nil
|
||||
case "OFF", "COMPLETED":
|
||||
return api.StatusB, nil
|
||||
case "ON":
|
||||
case "ON", "CHARGING":
|
||||
return api.StatusC, nil
|
||||
default:
|
||||
return api.StatusNone, errors.New("emobility - unknown charging state: " + res.BatteryChargeStatus.ChargingState)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -107,13 +180,26 @@ 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.emobilityG()
|
||||
res, err := v.mobileG()
|
||||
if res, ok := res.(StatusResponseMobile); err == nil && ok {
|
||||
m, err := res.MeasurementByKey("CLIMATIZER_STATE")
|
||||
if err != nil && err != api.ErrNotAvailable {
|
||||
return active, 20, 20, err
|
||||
}
|
||||
if err != api.ErrNotAvailable {
|
||||
return m.Value.IsOn, 20, 20, err
|
||||
}
|
||||
}
|
||||
|
||||
res, err = v.emobilityG()
|
||||
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
|
||||
default:
|
||||
return active, outsideTemp, targetTemp, errors.New("emobility - unknown climate state: " + res.DirectClimatisation.ClimatisationState)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -124,7 +210,18 @@ var _ api.VehicleOdometer = (*Provider)(nil)
|
|||
|
||||
// Odometer implements the api.VehicleOdometer interface
|
||||
func (v *Provider) Odometer() (float64, error) {
|
||||
res, err := v.statusG()
|
||||
res, err := v.mobileG()
|
||||
if res, ok := res.(StatusResponseMobile); err == nil && ok {
|
||||
m, err := res.MeasurementByKey("MILEAGE")
|
||||
if err != nil && err != api.ErrNotAvailable {
|
||||
return 0, err
|
||||
}
|
||||
if err != api.ErrNotAvailable {
|
||||
return float64(m.Value.Kilometers), nil
|
||||
}
|
||||
}
|
||||
|
||||
res, err = v.statusG()
|
||||
if res, ok := res.(StatusResponse); err == nil && ok {
|
||||
return res.Mileage.Value, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,11 @@
|
|||
package porsche
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/evcc-io/evcc/api"
|
||||
)
|
||||
|
||||
type Vehicle struct {
|
||||
VIN string
|
||||
ModelDescription string
|
||||
|
|
@ -20,6 +26,134 @@ type VehiclePairingResponse struct {
|
|||
CanSendPairingCode bool
|
||||
}
|
||||
|
||||
type MeasurementMobile struct {
|
||||
Key string
|
||||
/*
|
||||
RANGE,
|
||||
E_RANGE,
|
||||
MILEAGE,
|
||||
FUEL_LEVEL,
|
||||
FUEL_RESERVE,
|
||||
BATTERY_LEVEL // status, value
|
||||
BATTERY_CHARGING_STATE // status, value
|
||||
OIL_SERVICE_RANGE,
|
||||
OIL_SERVICE_TIME,
|
||||
MAIN_SERVICE_RANGE,
|
||||
MAIN_SERVICE_TIME,
|
||||
INTERMEDIATE_SERVICE_RANGE,
|
||||
INTERMEDIATE_SERVICE_TIME,
|
||||
OIL_LEVEL_MAX,
|
||||
OIL_LEVEL_CURRENT,
|
||||
OIL_LEVEL_MIN_WARNING,
|
||||
OPEN_STATE_DOOR_FRONT_LEFT,
|
||||
OPEN_STATE_DOOR_REAR_LEFT,
|
||||
OPEN_STATE_DOOR_FRONT_RIGHT,
|
||||
OPEN_STATE_DOOR_REAR_RIGHT,
|
||||
OPEN_STATE_LID_FRONT,
|
||||
OPEN_STATE_LID_REAR,
|
||||
OPEN_STATE_WINDOW_FRONT_LEFT,
|
||||
OPEN_STATE_WINDOW_REAR_LEFT,
|
||||
OPEN_STATE_WINDOW_FRONT_RIGHT,
|
||||
OPEN_STATE_WINDOW_REAR_RIGHT,
|
||||
OPEN_STATE_SUNROOF,
|
||||
OPEN_STATE_SUNROOF_REAR,
|
||||
OPEN_STATE_TOP,
|
||||
OPEN_STATE_SERVICE_FLAP,
|
||||
OPEN_STATE_SPOILER,
|
||||
OPEN_STATE_CHARGE_FLAP_LEFT,
|
||||
OPEN_STATE_CHARGE_FLAP_RIGHT,
|
||||
LOCK_STATE_VEHICLE,
|
||||
GPS_LOCATION,
|
||||
GLOBAL_PRIVACY_MODE,
|
||||
REMOTE_ACCESS_AUTHORIZATION,
|
||||
THEFT_MODE,
|
||||
TRIP_STATISTICS_CYCLIC,
|
||||
TRIP_STATISTICS_LONG_TERM,
|
||||
TRIP_STATISTICS_SHORT_TERM,
|
||||
PARKING_LIGHT,
|
||||
HEATING_STATE,
|
||||
ACV_STATE,
|
||||
CLIMATIZER_STATE // isOn, minutesLeft, targetTemperature
|
||||
TIMERS,
|
||||
BLEID_DDADATA,
|
||||
VTS_MODES,
|
||||
SPEED_ALARMS,
|
||||
LOCATION_ALARMS,
|
||||
CHARGING_PROFILES,
|
||||
BATTERY_TYPE // lastModified, plugTypes, capacityAh capacityKWh
|
||||
VALET_ALARM;
|
||||
*/
|
||||
Status struct {
|
||||
IsEnabled bool
|
||||
Cause string // AVAILABLE, PRIVACY_ACTIVATED, LICENSE_DEACTIVATED, NOT_SUPPORTED, UNKNOWN
|
||||
}
|
||||
Value struct {
|
||||
LastModified string
|
||||
|
||||
IsEnabled bool
|
||||
SoCPhoneNumber string
|
||||
Percent int64
|
||||
DistanceKM float64
|
||||
ZeroEmissionDistanceKm float64
|
||||
AvgLiterPerHundredKm float64
|
||||
AvgKwhPerHundredKm float64
|
||||
AvgSpeedKmh float64
|
||||
DrivingTimeMinutes float64
|
||||
TripEndTime string
|
||||
|
||||
IsOn bool
|
||||
TargetTemperature float64
|
||||
// MinutesLeft
|
||||
|
||||
Kilometers int64
|
||||
Days int64
|
||||
IsLocked bool
|
||||
IsOpen bool
|
||||
Direction int64
|
||||
|
||||
PlugTypes string // COMBINED_CS, TYP2, UNKNOWN
|
||||
// CapacityAh
|
||||
// CapacityKWh
|
||||
|
||||
Status string // CHARGING, FAST_CHARGING, CHARGING_COMPLETED, CHARGING_PAUSED, READY_TO_CHARGE, SOC_REACHED, INITIALISING, STANDBY, SUSPENDED, PLUGGED_LOCKED, PLUGGED_NOT_LOCKED, CHARGING_ERROR, NOT_PLUGGED, UNKNOWN
|
||||
Mode string // DIRECT, TIMER_PROFILE, LONGTERM, TCP, UNKNOWN
|
||||
DirectChargingState string // ENABLED_ON, ENABLED_OFF, DISABLED_ON, DISABLED_OFF, HIDDEN, UNKNOWN
|
||||
EndsAt string
|
||||
ChargingPower float64
|
||||
ChargingRate float64
|
||||
}
|
||||
}
|
||||
|
||||
type StatusResponseMobile struct {
|
||||
VIN string
|
||||
ModelName string
|
||||
CustomName string
|
||||
ModelType struct {
|
||||
Code string
|
||||
Year string
|
||||
Body string
|
||||
Generation string
|
||||
Model string
|
||||
Engine string
|
||||
}
|
||||
Connect bool
|
||||
GreyConnectStoreURL string
|
||||
PairingCodeV2 string
|
||||
Measurements []MeasurementMobile
|
||||
}
|
||||
|
||||
func (s *StatusResponseMobile) MeasurementByKey(key string) (*MeasurementMobile, error) {
|
||||
for _, m := range s.Measurements {
|
||||
if m.Key == key {
|
||||
if !m.Status.IsEnabled {
|
||||
return nil, errors.New(m.Status.Cause)
|
||||
}
|
||||
return &m, nil
|
||||
}
|
||||
}
|
||||
return nil, api.ErrNotAvailable
|
||||
}
|
||||
|
||||
type StatusResponse struct {
|
||||
BatteryLevel struct {
|
||||
Unit string
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue