218 lines
5.5 KiB
Go
218 lines
5.5 KiB
Go
package vw
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"github.com/evcc-io/evcc/util"
|
|
"github.com/evcc-io/evcc/util/request"
|
|
"golang.org/x/oauth2"
|
|
)
|
|
|
|
// DefaultBaseURI is the VW api base URI
|
|
const DefaultBaseURI = "https://msg.volkswagen.de/fs-car"
|
|
|
|
// RegionAPI is the VW api used for determining the home region
|
|
const RegionAPI = "https://mal-1a.prd.ece.vwg-connect.com/api"
|
|
|
|
// API is the VW api client
|
|
type API struct {
|
|
*request.Helper
|
|
brand, country string
|
|
baseURI string
|
|
statusURI string
|
|
}
|
|
|
|
// NewAPI creates a new api client
|
|
func NewAPI(log *util.Logger, ts oauth2.TokenSource, brand, country string) *API {
|
|
v := &API{
|
|
Helper: request.NewHelper(log),
|
|
brand: brand,
|
|
country: country,
|
|
baseURI: DefaultBaseURI,
|
|
}
|
|
|
|
v.Client.Transport = &oauth2.Transport{
|
|
Source: ts,
|
|
Base: v.Client.Transport,
|
|
}
|
|
|
|
return v
|
|
}
|
|
|
|
// HomeRegion updates the home region for the given vehicle
|
|
func (v *API) HomeRegion(vin string) error {
|
|
var res HomeRegion
|
|
uri := fmt.Sprintf("%s/cs/vds/v1/vehicles/%s/homeRegion", RegionAPI, vin)
|
|
|
|
err := v.GetJSON(uri, &res)
|
|
if err == nil {
|
|
if api := res.HomeRegion.BaseURI.Content; strings.HasPrefix(api, "https://mal-3a.prd.eu.dp.vwg-connect.com") {
|
|
api = "https://fal" + strings.TrimPrefix(api, "https://mal")
|
|
api = strings.TrimSuffix(api, "/api") + "/fs-car"
|
|
v.baseURI = api
|
|
}
|
|
} else if res.Error != nil {
|
|
err = res.Error.Error()
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
// RolesRights implements the /rolesrights/operationlist response
|
|
func (v *API) RolesRights(vin string) (RolesRights, error) {
|
|
var res RolesRights
|
|
uri := fmt.Sprintf("%s/rolesrights/operationlist/v3/vehicles/%s", RegionAPI, vin)
|
|
err := v.GetJSON(uri, &res)
|
|
return res, err
|
|
}
|
|
|
|
// ServiceURI renders the service URI for the given vin and service
|
|
func (v *API) ServiceURI(vin, service string, rr RolesRights) (uri string) {
|
|
if si := rr.ServiceByID(service); si != nil {
|
|
uri = si.InvocationUrl.Content
|
|
uri = strings.ReplaceAll(uri, "{vin}", vin)
|
|
uri = strings.ReplaceAll(uri, "{brand}", v.brand)
|
|
uri = strings.ReplaceAll(uri, "{country}", v.country)
|
|
}
|
|
|
|
return uri
|
|
}
|
|
|
|
// Status implements the /status response
|
|
func (v *API) Status(vin string) (StatusResponse, error) {
|
|
var res StatusResponse
|
|
uri := fmt.Sprintf("%s/bs/vsr/v1/vehicles/%s/status", RegionAPI, vin)
|
|
if v.statusURI != "" {
|
|
uri = v.statusURI
|
|
}
|
|
|
|
headers := map[string]string{
|
|
"Accept": request.JSONContent,
|
|
"X-App-Name": "foo", // required
|
|
"X-App-Version": "foo", // required
|
|
}
|
|
|
|
req, err := request.New(http.MethodGet, uri, nil, headers)
|
|
if err == nil {
|
|
err = v.DoJSON(req, &res)
|
|
}
|
|
|
|
if se := new(request.StatusError); errors.As(err, &se) {
|
|
var rr RolesRights
|
|
rr, err = v.RolesRights(vin)
|
|
|
|
if err == nil {
|
|
if uri = v.ServiceURI(vin, StatusService, rr); uri == "" {
|
|
err = fmt.Errorf("%s not found", StatusService)
|
|
}
|
|
}
|
|
|
|
if err == nil {
|
|
if strings.HasSuffix(uri, fmt.Sprintf("%s/", vin)) {
|
|
uri += "status"
|
|
}
|
|
|
|
if req, err = request.New(http.MethodGet, uri, nil, headers); err == nil {
|
|
if err = v.DoJSON(req, &res); err == nil {
|
|
v.statusURI = uri
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return res, err
|
|
}
|
|
|
|
// Charger implements the /charger response
|
|
func (v *API) Charger(vin string) (ChargerResponse, error) {
|
|
var res ChargerResponse
|
|
uri := fmt.Sprintf("%s/bs/batterycharge/v1/%s/%s/vehicles/%s/charger", v.baseURI, v.brand, v.country, vin)
|
|
err := v.GetJSON(uri, &res)
|
|
if err != nil && res.Error != nil {
|
|
err = res.Error.Error()
|
|
}
|
|
return res, err
|
|
}
|
|
|
|
// Climater implements the /climater response
|
|
func (v *API) Climater(vin string) (ClimaterResponse, error) {
|
|
var res ClimaterResponse
|
|
uri := fmt.Sprintf("%s/bs/climatisation/v1/%s/%s/vehicles/%s/climater", v.baseURI, v.brand, v.country, vin)
|
|
err := v.GetJSON(uri, &res)
|
|
if err != nil && res.Error != nil {
|
|
err = res.Error.Error()
|
|
}
|
|
return res, err
|
|
}
|
|
|
|
// Position implements the /position response
|
|
func (v *API) Position(vin string) (PositionResponse, error) {
|
|
var res PositionResponse
|
|
uri := fmt.Sprintf("%s/bs/cf/v1/%s/%s/vehicles/%s/position", v.baseURI, v.brand, v.country, vin)
|
|
|
|
req, err := request.New(http.MethodGet, uri, nil, map[string]string{
|
|
"Accept": request.JSONContent,
|
|
"Content-type": "application/vnd.vwg.mbb.carfinderservice_v1_0_0+json",
|
|
"X-App-Name": "foo", // required
|
|
"X-App-Version": "foo", // required
|
|
})
|
|
if err == nil {
|
|
err = v.DoJSON(req, &res)
|
|
}
|
|
|
|
if err != nil && res.Error != nil {
|
|
err = res.Error.Error()
|
|
}
|
|
|
|
return res, err
|
|
}
|
|
|
|
const (
|
|
ActionCharge = "batterycharge"
|
|
ActionChargeStart = "start"
|
|
ActionChargeStop = "stop"
|
|
)
|
|
|
|
type actionDefinition struct {
|
|
contentType string
|
|
appendix string
|
|
}
|
|
|
|
var actionDefinitions = map[string]actionDefinition{
|
|
ActionCharge: {
|
|
"application/vnd.vwg.mbb.ChargerAction_v1_0_0+xml",
|
|
"charger/actions",
|
|
},
|
|
}
|
|
|
|
// Action implements vehicle actions
|
|
func (v *API) Action(vin, action, value string) error {
|
|
def := actionDefinitions[action]
|
|
|
|
uri := fmt.Sprintf("%s/bs/%s/v1/%s/%s/vehicles/%s/%s", v.baseURI, action, v.brand, v.country, vin, def.appendix)
|
|
body := "<?xml version=\"1.0\" encoding=\"UTF-8\" ?><action><type>" + value + "</type></action>"
|
|
|
|
req, err := request.New(http.MethodPost, uri, strings.NewReader(body), map[string]string{
|
|
"Content-type": def.contentType,
|
|
})
|
|
|
|
if err == nil {
|
|
var resp *http.Response
|
|
if resp, err = v.Do(req); err == nil {
|
|
resp.Body.Close()
|
|
}
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
// Any implements any api response
|
|
func (v *API) Any(base, vin string) (any, error) {
|
|
var res any
|
|
uri := fmt.Sprintf("%s/"+strings.TrimLeft(base, "/"), v.baseURI, v.brand, v.country, vin)
|
|
err := v.GetJSON(uri, &res)
|
|
return res, err
|
|
}
|