Ford: refactor (#1898)
This commit is contained in:
parent
5e586fcea4
commit
1194568cf2
5 changed files with 383 additions and 276 deletions
302
vehicle/ford.go
302
vehicle/ford.go
|
|
@ -1,40 +1,28 @@
|
|||
package vehicle
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"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/oauth"
|
||||
"github.com/evcc-io/evcc/util/request"
|
||||
"golang.org/x/oauth2"
|
||||
"github.com/evcc-io/evcc/vehicle/ford"
|
||||
)
|
||||
|
||||
const (
|
||||
fordAuth = "https://fcis.ice.ibmcloud.com"
|
||||
fordAPI = "https://usapi.cv.ford.com"
|
||||
fordVehicleList = "https://api.mps.ford.com/api/users/vehicles"
|
||||
fordStatusExpiry = 5 * time.Minute // if returned status value is older, evcc will init refresh
|
||||
fordRefreshTimeout = time.Minute // timeout to get status after refresh
|
||||
fordTimeFormat = "01-02-2006 15:04:05" // time format used by Ford API, time is in UTC
|
||||
)
|
||||
// https://github.com/d4v3y0rk/ffpass-module
|
||||
|
||||
// Ford is an api.Vehicle implementation for Ford cars
|
||||
type Ford struct {
|
||||
*embed
|
||||
*request.Helper
|
||||
log *util.Logger
|
||||
user, password, vin string
|
||||
tokenSource oauth2.TokenSource
|
||||
statusG func() (interface{}, error)
|
||||
refreshId string
|
||||
refreshTime time.Time
|
||||
// *request.Helper
|
||||
// log *util.Logger
|
||||
// vin string
|
||||
// tokenSource oauth2.TokenSource
|
||||
// statusG func() (interface{}, error)
|
||||
// refreshId string
|
||||
// refreshTime time.Time
|
||||
*ford.Provider
|
||||
}
|
||||
|
||||
func init() {
|
||||
|
|
@ -46,9 +34,11 @@ func NewFordFromConfig(other map[string]interface{}) (api.Vehicle, error) {
|
|||
cc := struct {
|
||||
embed `mapstructure:",squash"`
|
||||
User, Password, VIN string
|
||||
Expiry time.Duration
|
||||
Cache time.Duration
|
||||
}{
|
||||
Cache: interval,
|
||||
Expiry: expiry,
|
||||
Cache: interval,
|
||||
}
|
||||
|
||||
if err := util.DecodeOther(other, &cc); err != nil {
|
||||
|
|
@ -59,268 +49,28 @@ func NewFordFromConfig(other map[string]interface{}) (api.Vehicle, error) {
|
|||
return nil, api.ErrMissingCredentials
|
||||
}
|
||||
|
||||
log := util.NewLogger("ford").Redact(cc.User, cc.Password, cc.VIN)
|
||||
|
||||
v := &Ford{
|
||||
embed: &cc.embed,
|
||||
Helper: request.NewHelper(log),
|
||||
log: log,
|
||||
user: cc.User,
|
||||
password: cc.Password,
|
||||
vin: strings.ToUpper(cc.VIN),
|
||||
embed: &cc.embed,
|
||||
}
|
||||
|
||||
token, err := v.login()
|
||||
if err == nil {
|
||||
v.tokenSource = oauth.RefreshTokenSource((*oauth2.Token)(&token), v)
|
||||
log := util.NewLogger("ford").Redact(cc.User, cc.Password, cc.VIN)
|
||||
identity := ford.NewIdentity(log, cc.User, cc.Password)
|
||||
|
||||
err := identity.Login()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("login failed: %w", err)
|
||||
}
|
||||
|
||||
v.statusG = provider.NewCached(func() (interface{}, error) {
|
||||
return v.status()
|
||||
}, cc.Cache).InterfaceGetter()
|
||||
api := ford.NewAPI(log, identity)
|
||||
|
||||
if err == nil && cc.VIN == "" {
|
||||
v.vin, err = findVehicle(v.vehicles())
|
||||
if cc.VIN == "" {
|
||||
cc.VIN, err = findVehicle(api.Vehicles())
|
||||
if err == nil {
|
||||
log.DEBUG.Printf("found vehicle: %v", v.vin)
|
||||
log.DEBUG.Printf("found vehicle: %v", cc.VIN)
|
||||
}
|
||||
}
|
||||
|
||||
v.Provider = ford.NewProvider(api, strings.ToUpper(cc.VIN), cc.Expiry, cc.Cache)
|
||||
|
||||
return v, err
|
||||
}
|
||||
|
||||
// login authenticates with username/password to get new token
|
||||
func (v *Ford) login() (oauth.Token, error) {
|
||||
data := url.Values{
|
||||
"client_id": []string{"9fb503e0-715b-47e8-adfd-ad4b7770f73b"},
|
||||
"grant_type": []string{"password"},
|
||||
"username": []string{v.user},
|
||||
"password": []string{v.password},
|
||||
}
|
||||
|
||||
uri := fordAuth + "/v1.0/endpoint/default/token"
|
||||
req, err := request.New(http.MethodPost, uri, strings.NewReader(data.Encode()), request.URLEncoding)
|
||||
|
||||
var res oauth.Token
|
||||
if err == nil {
|
||||
err = v.DoJSON(req, &res)
|
||||
}
|
||||
|
||||
return res, err
|
||||
}
|
||||
|
||||
// Refresh implements oauth.TokenRefresher
|
||||
func (v *Ford) RefreshToken(token *oauth2.Token) (*oauth2.Token, error) {
|
||||
data := url.Values{
|
||||
"client_id": []string{"9fb503e0-715b-47e8-adfd-ad4b7770f73b"},
|
||||
"grant_type": []string{"refresh_token"},
|
||||
"refresh_token": []string{token.RefreshToken},
|
||||
}
|
||||
|
||||
uri := fordAuth + "/v1.0/endpoint/default/token"
|
||||
req, err := request.New(http.MethodPost, uri, strings.NewReader(data.Encode()), request.URLEncoding)
|
||||
|
||||
var res oauth.Token
|
||||
if err == nil {
|
||||
err = v.DoJSON(req, &res)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
res, err = v.login()
|
||||
}
|
||||
|
||||
return (*oauth2.Token)(&res), err
|
||||
}
|
||||
|
||||
// request is a helper to send API requests, sets header the Ford API expects
|
||||
func (v *Ford) request(method, uri string) (*http.Request, error) {
|
||||
token, err := v.tokenSource.Token()
|
||||
|
||||
var req *http.Request
|
||||
if err == nil {
|
||||
req, err = request.New(method, uri, nil, map[string]string{
|
||||
"Content-type": "application/json",
|
||||
"Application-Id": "71A3AD0A-CF46-4CCF-B473-FC7FE5BC4592",
|
||||
"Auth-Token": token.AccessToken,
|
||||
})
|
||||
}
|
||||
|
||||
return req, err
|
||||
}
|
||||
|
||||
// fordVehicleStatus holds the relevant data extracted from JSON that the server sends
|
||||
// on vehicle status request
|
||||
type fordVehicleStatus struct {
|
||||
VehicleStatus struct {
|
||||
BatteryFillLevel struct {
|
||||
Value float64
|
||||
Timestamp string
|
||||
}
|
||||
ElVehDTE struct {
|
||||
Value float64
|
||||
Timestamp string
|
||||
}
|
||||
ChargingStatus struct {
|
||||
Value string
|
||||
Timestamp string
|
||||
}
|
||||
PlugStatus struct {
|
||||
Value int
|
||||
Timestamp string
|
||||
}
|
||||
LastRefresh string
|
||||
}
|
||||
Status int
|
||||
}
|
||||
|
||||
// vehicles returns the list of user vehicles
|
||||
func (v *Ford) vehicles() ([]string, error) {
|
||||
var resp struct {
|
||||
Vehicles struct {
|
||||
Values []struct {
|
||||
VIN string
|
||||
} `json:"$values"`
|
||||
}
|
||||
}
|
||||
|
||||
req, err := v.request(http.MethodGet, fordVehicleList)
|
||||
if err == nil {
|
||||
err = v.DoJSON(req, &resp)
|
||||
}
|
||||
|
||||
var vehicles []string
|
||||
if err == nil {
|
||||
for _, v := range resp.Vehicles.Values {
|
||||
vehicles = append(vehicles, v.VIN)
|
||||
}
|
||||
}
|
||||
|
||||
return vehicles, err
|
||||
}
|
||||
|
||||
// status performs a /status request to the Ford API and triggers a refresh if
|
||||
// the received status is too old
|
||||
func (v *Ford) status() (res fordVehicleStatus, err error) {
|
||||
// follow up requested refresh
|
||||
if v.refreshId != "" {
|
||||
return v.refreshResult()
|
||||
}
|
||||
|
||||
// otherwise start normal workflow
|
||||
uri := fmt.Sprintf("%s/api/vehicles/v3/%s/status", fordAPI, v.vin)
|
||||
req, err := v.request(http.MethodGet, uri)
|
||||
if err == nil {
|
||||
err = v.DoJSON(req, &res)
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
var lastUpdate time.Time
|
||||
lastUpdate, err = time.Parse(fordTimeFormat, res.VehicleStatus.LastRefresh)
|
||||
|
||||
if elapsed := time.Since(lastUpdate); err == nil && elapsed > fordStatusExpiry {
|
||||
v.log.DEBUG.Printf("vehicle status is outdated (age %v > %v), requesting refresh", elapsed, fordStatusExpiry)
|
||||
|
||||
if err = v.refreshRequest(); err == nil {
|
||||
err = api.ErrMustRetry
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return res, err
|
||||
}
|
||||
|
||||
// refreshResult triggers an update if not already in progress, otherwise gets result
|
||||
func (v *Ford) refreshResult() (res fordVehicleStatus, err error) {
|
||||
uri := fmt.Sprintf("%s/api/vehicles/v3/%s/statusrefresh/%s", fordAPI, v.vin, v.refreshId)
|
||||
|
||||
var req *http.Request
|
||||
if req, err = v.request(http.MethodGet, uri); err == nil {
|
||||
err = v.DoJSON(req, &res)
|
||||
}
|
||||
|
||||
// update successful and completed
|
||||
if err == nil && res.Status == 200 {
|
||||
v.refreshId = ""
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// update still in progress, keep retrying
|
||||
if time.Since(v.refreshTime) < fordRefreshTimeout {
|
||||
return res, api.ErrMustRetry
|
||||
}
|
||||
|
||||
// give up
|
||||
v.refreshId = ""
|
||||
if err == nil {
|
||||
err = api.ErrTimeout
|
||||
}
|
||||
|
||||
return res, err
|
||||
}
|
||||
|
||||
// refreshRequest requests status refresh tracked by commandId
|
||||
func (v *Ford) refreshRequest() error {
|
||||
var resp struct {
|
||||
CommandId string
|
||||
}
|
||||
|
||||
uri := fmt.Sprintf("%s/api/vehicles/v2/%s/status", fordAPI, v.vin)
|
||||
req, err := v.request(http.MethodPut, uri)
|
||||
if err == nil {
|
||||
err = v.DoJSON(req, &resp)
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
v.refreshId = resp.CommandId
|
||||
v.refreshTime = time.Now()
|
||||
|
||||
if resp.CommandId == "" {
|
||||
err = errors.New("refresh failed")
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
var _ api.Battery = (*Ford)(nil)
|
||||
|
||||
// SoC implements the api.Battery interface
|
||||
func (v *Ford) SoC() (float64, error) {
|
||||
res, err := v.statusG()
|
||||
if res, ok := res.(fordVehicleStatus); err == nil && ok {
|
||||
return res.VehicleStatus.BatteryFillLevel.Value, nil
|
||||
}
|
||||
|
||||
return 0, err
|
||||
}
|
||||
|
||||
var _ api.VehicleRange = (*Ford)(nil)
|
||||
|
||||
// Range implements the api.VehicleRange interface
|
||||
func (v *Ford) Range() (int64, error) {
|
||||
res, err := v.statusG()
|
||||
if res, ok := res.(fordVehicleStatus); err == nil && ok {
|
||||
return int64(res.VehicleStatus.ElVehDTE.Value), nil
|
||||
}
|
||||
|
||||
return 0, err
|
||||
}
|
||||
|
||||
var _ api.ChargeState = (*Ford)(nil)
|
||||
|
||||
// Status implements the api.ChargeState interface
|
||||
func (v *Ford) Status() (api.ChargeStatus, error) {
|
||||
status := api.StatusA // disconnected
|
||||
|
||||
res, err := v.statusG()
|
||||
if res, ok := res.(fordVehicleStatus); err == nil && ok {
|
||||
if res.VehicleStatus.PlugStatus.Value == 1 {
|
||||
status = api.StatusB // connected, not charging
|
||||
}
|
||||
if res.VehicleStatus.ChargingStatus.Value == "ChargingAC" {
|
||||
status = api.StatusC // charging
|
||||
}
|
||||
}
|
||||
|
||||
return status, err
|
||||
}
|
||||
|
|
|
|||
102
vehicle/ford/api.go
Normal file
102
vehicle/ford/api.go
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
package ford
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"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 (
|
||||
ApiURI = "https://usapi.cv.ford.com"
|
||||
VehicleListURI = "https://api.mps.ford.com/api/users/vehicles"
|
||||
)
|
||||
|
||||
// API is the VW api client
|
||||
type API struct {
|
||||
*request.Helper
|
||||
}
|
||||
|
||||
// NewAPI creates a new api client
|
||||
func NewAPI(log *util.Logger, ts oauth2.TokenSource) *API {
|
||||
v := &API{
|
||||
Helper: request.NewHelper(log),
|
||||
}
|
||||
|
||||
v.Client.Transport = &transport.Decorator{
|
||||
Decorator: func(req *http.Request) error {
|
||||
token, err := ts.Token()
|
||||
if err == nil {
|
||||
for k, v := range map[string]string{
|
||||
"Content-type": "application/json",
|
||||
"Application-Id": "71A3AD0A-CF46-4CCF-B473-FC7FE5BC4592",
|
||||
"Auth-Token": token.AccessToken,
|
||||
} {
|
||||
req.Header.Set(k, v)
|
||||
}
|
||||
}
|
||||
return err
|
||||
},
|
||||
Base: v.Client.Transport,
|
||||
}
|
||||
|
||||
return v
|
||||
}
|
||||
|
||||
// Vehicles returns the list of user vehicles
|
||||
func (v *API) Vehicles() ([]string, error) {
|
||||
var resp VehiclesResponse
|
||||
var vehicles []string
|
||||
|
||||
err := v.GetJSON(VehicleListURI, &resp)
|
||||
if err == nil {
|
||||
for _, v := range resp.Vehicles.Values {
|
||||
vehicles = append(vehicles, v.VIN)
|
||||
}
|
||||
}
|
||||
|
||||
return vehicles, err
|
||||
}
|
||||
|
||||
// Status performs a /status request
|
||||
func (v *API) Status(vin string) (StatusResponse, error) {
|
||||
uri := fmt.Sprintf("%s/api/vehicles/v3/%s/status", ApiURI, vin)
|
||||
|
||||
var res StatusResponse
|
||||
err := v.GetJSON(uri, &res)
|
||||
|
||||
return res, err
|
||||
}
|
||||
|
||||
// RefreshResult retrieves a refresh result using /statusrefresh
|
||||
func (v *API) RefreshResult(vin, refreshId string) (StatusResponse, error) {
|
||||
var res StatusResponse
|
||||
|
||||
uri := fmt.Sprintf("%s/api/vehicles/v3/%s/statusrefresh/%s", ApiURI, vin, refreshId)
|
||||
err := v.GetJSON(uri, &res)
|
||||
|
||||
return res, err
|
||||
}
|
||||
|
||||
// RefreshRequest requests status refresh tracked by commandId
|
||||
func (v *API) RefreshRequest(vin string) (string, error) {
|
||||
var resp struct {
|
||||
CommandId string
|
||||
}
|
||||
|
||||
uri := fmt.Sprintf("%s/api/vehicles/v2/%s/status", ApiURI, vin)
|
||||
req, err := http.NewRequest(http.MethodPut, uri, nil)
|
||||
if err == nil {
|
||||
err = v.DoJSON(req, &resp)
|
||||
}
|
||||
|
||||
if err == nil && resp.CommandId == "" {
|
||||
err = errors.New("refresh failed")
|
||||
}
|
||||
|
||||
return resp.CommandId, err
|
||||
}
|
||||
84
vehicle/ford/identity.go
Normal file
84
vehicle/ford/identity.go
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
package ford
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/evcc-io/evcc/util"
|
||||
"github.com/evcc-io/evcc/util/oauth"
|
||||
"github.com/evcc-io/evcc/util/request"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
const (
|
||||
AuthURI = "https://sso.ci.ford.ca" // fcis.ice.ibmcloud.com
|
||||
ClientID = "9fb503e0-715b-47e8-adfd-ad4b7770f73b"
|
||||
)
|
||||
|
||||
type Identity struct {
|
||||
*request.Helper
|
||||
user, password string
|
||||
oauth2.TokenSource
|
||||
}
|
||||
|
||||
// NewIdentity creates Fiat identity
|
||||
func NewIdentity(log *util.Logger, user, password string) *Identity {
|
||||
return &Identity{
|
||||
Helper: request.NewHelper(log),
|
||||
user: user,
|
||||
password: password,
|
||||
}
|
||||
}
|
||||
|
||||
// Login authenticates with username/password to get new aws credentials
|
||||
func (v *Identity) Login() error {
|
||||
token, err := v.login()
|
||||
if err == nil {
|
||||
v.TokenSource = oauth.RefreshTokenSource((*oauth2.Token)(&token), v)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// login authenticates with username/password to get new token
|
||||
func (v *Identity) login() (oauth.Token, error) {
|
||||
data := url.Values{
|
||||
"client_id": []string{ClientID},
|
||||
"grant_type": []string{"password"},
|
||||
"username": []string{v.user},
|
||||
"password": []string{v.password},
|
||||
}
|
||||
|
||||
uri := AuthURI + "/v1.0/endpoint/default/token"
|
||||
req, err := request.New(http.MethodPost, uri, strings.NewReader(data.Encode()), request.URLEncoding)
|
||||
|
||||
var res oauth.Token
|
||||
if err == nil {
|
||||
err = v.DoJSON(req, &res)
|
||||
}
|
||||
|
||||
return res, err
|
||||
}
|
||||
|
||||
// Refresh implements oauth.TokenRefresher
|
||||
func (v *Identity) RefreshToken(token *oauth2.Token) (*oauth2.Token, error) {
|
||||
data := url.Values{
|
||||
"client_id": []string{ClientID},
|
||||
"grant_type": []string{"refresh_token"},
|
||||
"refresh_token": []string{token.RefreshToken},
|
||||
}
|
||||
|
||||
uri := AuthURI + "/v1.0/endpoint/default/token"
|
||||
req, err := request.New(http.MethodPost, uri, strings.NewReader(data.Encode()), request.URLEncoding)
|
||||
|
||||
var res oauth.Token
|
||||
if err == nil {
|
||||
err = v.DoJSON(req, &res)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
res, err = v.login()
|
||||
}
|
||||
|
||||
return (*oauth2.Token)(&res), err
|
||||
}
|
||||
114
vehicle/ford/provider.go
Normal file
114
vehicle/ford/provider.go
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
package ford
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/evcc-io/evcc/api"
|
||||
"github.com/evcc-io/evcc/provider"
|
||||
)
|
||||
|
||||
const refreshTimeout = time.Minute
|
||||
|
||||
type Provider struct {
|
||||
statusG func() (interface{}, error)
|
||||
expiry time.Duration
|
||||
refreshTime time.Time
|
||||
refreshId string
|
||||
}
|
||||
|
||||
func NewProvider(api *API, vin string, expiry, cache time.Duration) *Provider {
|
||||
impl := &Provider{
|
||||
expiry: expiry,
|
||||
}
|
||||
|
||||
impl.statusG = provider.NewCached(func() (interface{}, error) {
|
||||
return impl.status(
|
||||
func() (StatusResponse, error) { return api.Status(vin) },
|
||||
func(id string) (StatusResponse, error) { return api.RefreshResult(vin, id) },
|
||||
func() (string, error) { return api.RefreshRequest(vin) },
|
||||
)
|
||||
}, cache).InterfaceGetter()
|
||||
|
||||
return impl
|
||||
}
|
||||
|
||||
func (v *Provider) status(
|
||||
statusG func() (StatusResponse, error),
|
||||
refreshG func(id string) (StatusResponse, error),
|
||||
refreshRequest func() (string, error),
|
||||
) (StatusResponse, error) {
|
||||
if v.refreshId != "" {
|
||||
res, err := refreshG(v.refreshId)
|
||||
|
||||
// update successful and completed
|
||||
if err == nil {
|
||||
v.refreshId = ""
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// update still in progress, keep retrying
|
||||
if time.Since(v.refreshTime) < refreshTimeout {
|
||||
return res, api.ErrMustRetry
|
||||
}
|
||||
|
||||
// give up
|
||||
v.refreshId = ""
|
||||
return res, api.ErrTimeout
|
||||
}
|
||||
|
||||
res, err := statusG()
|
||||
|
||||
if err == nil {
|
||||
if time.Since(res.VehicleStatus.LastRefresh.Time) > v.expiry {
|
||||
if v.refreshId, err = refreshRequest(); err == nil {
|
||||
v.refreshTime = time.Now()
|
||||
err = api.ErrMustRetry
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return res, err
|
||||
}
|
||||
|
||||
var _ api.Battery = (*Provider)(nil)
|
||||
|
||||
// SoC implements the api.Battery interface
|
||||
func (v *Provider) SoC() (float64, error) {
|
||||
res, err := v.statusG()
|
||||
if res, ok := res.(StatusResponse); err == nil && ok {
|
||||
return res.VehicleStatus.BatteryFillLevel.Value, nil
|
||||
}
|
||||
|
||||
return 0, err
|
||||
}
|
||||
|
||||
var _ api.VehicleRange = (*Provider)(nil)
|
||||
|
||||
// Range implements the api.VehicleRange interface
|
||||
func (v *Provider) Range() (int64, error) {
|
||||
res, err := v.statusG()
|
||||
if res, ok := res.(StatusResponse); err == nil && ok {
|
||||
return int64(res.VehicleStatus.ElVehDTE.Value), nil
|
||||
}
|
||||
|
||||
return 0, err
|
||||
}
|
||||
|
||||
var _ api.ChargeState = (*Provider)(nil)
|
||||
|
||||
// Status implements the api.ChargeState interface
|
||||
func (v *Provider) Status() (api.ChargeStatus, error) {
|
||||
status := api.StatusA // disconnected
|
||||
|
||||
res, err := v.statusG()
|
||||
if res, ok := res.(StatusResponse); err == nil && ok {
|
||||
if res.VehicleStatus.PlugStatus.Value == 1 {
|
||||
status = api.StatusB // connected, not charging
|
||||
}
|
||||
if res.VehicleStatus.ChargingStatus.Value == "ChargingAC" {
|
||||
status = api.StatusC // charging
|
||||
}
|
||||
}
|
||||
|
||||
return status, err
|
||||
}
|
||||
57
vehicle/ford/types.go
Normal file
57
vehicle/ford/types.go
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
package ford
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type VehiclesResponse struct {
|
||||
Vehicles struct {
|
||||
Values []struct {
|
||||
VIN string
|
||||
} `json:"$values"`
|
||||
}
|
||||
}
|
||||
|
||||
// StatusResponse is the response to the vehicle status request
|
||||
type StatusResponse struct {
|
||||
VehicleStatus struct {
|
||||
BatteryFillLevel struct {
|
||||
Value float64
|
||||
Timestamp Timestamp
|
||||
}
|
||||
ElVehDTE struct {
|
||||
Value float64
|
||||
Timestamp Timestamp
|
||||
}
|
||||
ChargingStatus struct {
|
||||
Value string
|
||||
Timestamp Timestamp
|
||||
}
|
||||
PlugStatus struct {
|
||||
Value int
|
||||
Timestamp Timestamp
|
||||
}
|
||||
LastRefresh Timestamp
|
||||
}
|
||||
Status int
|
||||
}
|
||||
|
||||
const TimeFormat = "01-02-2006 15:04:05" // time format used by Ford API, time is in UTC
|
||||
|
||||
// Timestamp implements JSON unmarshal
|
||||
type Timestamp struct {
|
||||
time.Time
|
||||
}
|
||||
|
||||
// UnmarshalJSON decodes string timestamp into time.Time
|
||||
func (ct *Timestamp) UnmarshalJSON(data []byte) error {
|
||||
s := strings.Trim(string(data), "\"")
|
||||
|
||||
t, err := time.Parse(TimeFormat, s)
|
||||
if err == nil {
|
||||
(*ct).Time = t
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue