Skoda: replace obsolete Skoda api with Enyaq (#19127)
This commit is contained in:
parent
4c73f18270
commit
1891a58ce5
12 changed files with 16 additions and 533 deletions
|
|
@ -1,4 +1,5 @@
|
|||
template: enyaq
|
||||
covers: ["skoda"]
|
||||
products:
|
||||
- brand: Skoda
|
||||
description:
|
||||
|
|
|
|||
|
|
@ -1,11 +0,0 @@
|
|||
template: skoda
|
||||
products:
|
||||
- brand: Skoda
|
||||
description:
|
||||
de: Andere
|
||||
en: Others
|
||||
params:
|
||||
- preset: vehicle-base
|
||||
render: |
|
||||
type: skoda
|
||||
{{ include "vehicle-base" . }}
|
||||
|
|
@ -43,6 +43,10 @@ type StatusError struct {
|
|||
resp *http.Response
|
||||
}
|
||||
|
||||
func NewStatusError(resp *http.Response) *StatusError {
|
||||
return &StatusError{resp: resp}
|
||||
}
|
||||
|
||||
func (e *StatusError) Error() string {
|
||||
return fmt.Sprintf("unexpected status: %d (%s)", e.resp.StatusCode, http.StatusText(e.resp.StatusCode))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,84 +0,0 @@
|
|||
package vehicle
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/evcc-io/evcc/api"
|
||||
"github.com/evcc-io/evcc/util"
|
||||
"github.com/evcc-io/evcc/util/request"
|
||||
"github.com/evcc-io/evcc/vehicle/skoda"
|
||||
"github.com/evcc-io/evcc/vehicle/vag/service"
|
||||
"github.com/evcc-io/evcc/vehicle/vw"
|
||||
)
|
||||
|
||||
// https://github.com/trocotronic/weconnect
|
||||
// https://github.com/TA2k/ioBroker.vw-connect
|
||||
|
||||
// Skoda is an api.Vehicle implementation for Skoda cars
|
||||
type Skoda struct {
|
||||
*embed
|
||||
*vw.Provider // provides the api implementations
|
||||
}
|
||||
|
||||
func init() {
|
||||
registry.Add("skoda", NewSkodaFromConfig)
|
||||
}
|
||||
|
||||
// NewSkodaFromConfig creates a new vehicle
|
||||
func NewSkodaFromConfig(other map[string]interface{}) (api.Vehicle, error) {
|
||||
cc := struct {
|
||||
embed `mapstructure:",squash"`
|
||||
User, Password, VIN string
|
||||
Cache time.Duration
|
||||
Timeout time.Duration
|
||||
}{
|
||||
Cache: interval,
|
||||
Timeout: request.Timeout,
|
||||
}
|
||||
|
||||
if err := util.DecodeOther(other, &cc); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if cc.User == "" || cc.Password == "" {
|
||||
return nil, api.ErrMissingCredentials
|
||||
}
|
||||
|
||||
v := &Skoda{
|
||||
embed: &cc.embed,
|
||||
}
|
||||
|
||||
log := util.NewLogger("skoda").Redact(cc.User, cc.Password, cc.VIN)
|
||||
|
||||
// use Skoda api to resolve list of vehicles
|
||||
trs, err := service.TokenRefreshServiceTokenSource(log, skoda.TRSParams, skoda.AuthParams, cc.User, cc.Password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
api := skoda.NewAPI(log, trs)
|
||||
api.Client.Timeout = cc.Timeout
|
||||
|
||||
vehicle, err := ensureVehicleEx(
|
||||
cc.VIN, api.Vehicles,
|
||||
func(v skoda.Vehicle) (string, error) {
|
||||
return v.VIN, nil
|
||||
},
|
||||
)
|
||||
|
||||
if err == nil {
|
||||
v.fromVehicle(vehicle.Name, float64(vehicle.Specification.Battery.CapacityInKWh))
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
ts := service.MbbTokenSource(log, trs, skoda.AuthClientID)
|
||||
api := vw.NewAPI(log, ts, skoda.Brand, skoda.Country)
|
||||
api.Client.Timeout = cc.Timeout
|
||||
|
||||
if err = api.HomeRegion(vehicle.VIN); err == nil {
|
||||
v.Provider = vw.NewProvider(api, vehicle.VIN, cc.Cache)
|
||||
}
|
||||
}
|
||||
|
||||
return v, err
|
||||
}
|
||||
|
|
@ -1,91 +0,0 @@
|
|||
package skoda
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/evcc-io/evcc/util"
|
||||
"github.com/evcc-io/evcc/util/request"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
const BaseURI = "https://api.connect.skoda-auto.cz/api"
|
||||
|
||||
// API is the Skoda 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 = &oauth2.Transport{
|
||||
Source: ts,
|
||||
Base: v.Client.Transport,
|
||||
}
|
||||
|
||||
return v
|
||||
}
|
||||
|
||||
// Vehicles implements the /v3/garage response
|
||||
func (v *API) Vehicles() ([]Vehicle, error) {
|
||||
var res VehiclesResponse
|
||||
|
||||
uri := fmt.Sprintf("%s/v3/garage", BaseURI)
|
||||
err := v.GetJSON(uri, &res)
|
||||
|
||||
return res.Vehicles, err
|
||||
}
|
||||
|
||||
// Status implements the /v2/vehicle-status/<vin> response
|
||||
func (v *API) Status(vin string) (StatusResponse, error) {
|
||||
var res StatusResponse
|
||||
uri := fmt.Sprintf("%s/v2/vehicle-status/%s", BaseURI, vin)
|
||||
err := v.GetJSON(uri, &res)
|
||||
return res, err
|
||||
}
|
||||
|
||||
// Charger implements the /v1/charging/<vin>/status response
|
||||
func (v *API) Charger(vin string) (ChargerResponse, error) {
|
||||
var res ChargerResponse
|
||||
uri := fmt.Sprintf("%s/v1/charging/%s/status", BaseURI, vin)
|
||||
err := v.GetJSON(uri, &res)
|
||||
return res, err
|
||||
}
|
||||
|
||||
// Settings implements the /v1/charging/<vin>/settings response
|
||||
func (v *API) Settings(vin string) (SettingsResponse, error) {
|
||||
var res SettingsResponse
|
||||
uri := fmt.Sprintf("%s/v1/charging/%s/settings", BaseURI, vin)
|
||||
err := v.GetJSON(uri, &res)
|
||||
return res, err
|
||||
}
|
||||
|
||||
const (
|
||||
ActionCharge = "charging"
|
||||
ActionChargeStart = "Start"
|
||||
ActionChargeStop = "Stop"
|
||||
)
|
||||
|
||||
// Action executes a vehicle action
|
||||
func (v *API) Action(vin, action, value string) error {
|
||||
var res map[string]interface{}
|
||||
uri := fmt.Sprintf("%s/v1/%s/operation-requests?vin=%s", BaseURI, action, vin)
|
||||
|
||||
data := struct {
|
||||
Typ string `json:"type"`
|
||||
}{
|
||||
Typ: value,
|
||||
}
|
||||
|
||||
req, err := request.New(http.MethodPost, uri, request.MarshalJSON(data), request.JSONEncoding)
|
||||
if err == nil {
|
||||
// {"id":"61991908906fa40af9a5cba4","status":"InProgress","deeplink":""}
|
||||
err = v.DoJSON(req, &res)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
|
@ -9,7 +9,10 @@ import (
|
|||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
const BaseURI = "https://mysmob.api.connect.skoda-auto.cz/api"
|
||||
const (
|
||||
BaseURI = "https://mysmob.api.connect.skoda-auto.cz/api"
|
||||
AllGen = "connectivityGenerations=MOD1&connectivityGenerations=MOD2&connectivityGenerations=MOD3&connectivityGenerations=MOD4"
|
||||
)
|
||||
|
||||
// API is the Skoda api client
|
||||
type API struct {
|
||||
|
|
@ -34,7 +37,7 @@ func NewAPI(log *util.Logger, ts oauth2.TokenSource) *API {
|
|||
func (v *API) Vehicles() ([]Vehicle, error) {
|
||||
var res VehiclesResponse
|
||||
|
||||
uri := fmt.Sprintf("%s/v2/garage", BaseURI)
|
||||
uri := fmt.Sprintf("%s/v2/garage?%s", BaseURI, AllGen)
|
||||
err := v.GetJSON(uri, &res)
|
||||
|
||||
return res.Vehicles, err
|
||||
|
|
@ -42,7 +45,7 @@ func (v *API) Vehicles() ([]Vehicle, error) {
|
|||
|
||||
func (v *API) VehicleDetails(vin string) (Vehicle, error) {
|
||||
var res Vehicle
|
||||
uri := fmt.Sprintf("%s/v2/garage/vehicles/%s", BaseURI, vin)
|
||||
uri := fmt.Sprintf("%s/v2/garage/vehicles/%s?%s", BaseURI, vin, AllGen)
|
||||
err := v.GetJSON(uri, &res)
|
||||
return res, err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -58,9 +58,13 @@ func (v *Service) Exchange(q url.Values) (*vag.Token, error) {
|
|||
}
|
||||
|
||||
resp, err := v.Post(CodeExchangeURL, request.JSONContent, request.MarshalJSON(data))
|
||||
|
||||
if err == nil {
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != 200 {
|
||||
return nil, request.NewStatusError(resp)
|
||||
}
|
||||
}
|
||||
if err == nil {
|
||||
json.NewDecoder(resp.Body).Decode(&skTok)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,24 +0,0 @@
|
|||
package skoda
|
||||
|
||||
import "net/url"
|
||||
|
||||
const (
|
||||
Brand = "VW"
|
||||
Country = "CZ"
|
||||
|
||||
// Authorization ClientID
|
||||
AuthClientID = "afb0473b-6d82-42b8-bfea-cead338c46ef"
|
||||
)
|
||||
|
||||
// Skoda native api
|
||||
var AuthParams = url.Values{
|
||||
"response_type": {"code id_token"},
|
||||
"client_id": {"f9a2359a-b776-46d9-bd0c-db1904343117@apps_vw-dilab_com"},
|
||||
"redirect_uri": {"skodaconnect://oidc.login/"},
|
||||
"scope": {"openid mbb profile"},
|
||||
}
|
||||
|
||||
// TokenRefreshService parameters
|
||||
var TRSParams = url.Values{
|
||||
"brand": {"skoda"},
|
||||
}
|
||||
|
|
@ -1,146 +0,0 @@
|
|||
package skoda
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/evcc-io/evcc/api"
|
||||
"github.com/evcc-io/evcc/util"
|
||||
)
|
||||
|
||||
// Provider implements the vehicle api
|
||||
type Provider struct {
|
||||
statusG func() (StatusResponse, error)
|
||||
chargerG func() (ChargerResponse, error)
|
||||
settingsG func() (SettingsResponse, error)
|
||||
action func(action, value string) error
|
||||
}
|
||||
|
||||
// NewProvider creates a vehicle api provider
|
||||
func NewProvider(api *API, vin string, cache time.Duration) *Provider {
|
||||
impl := &Provider{
|
||||
statusG: util.Cached(func() (StatusResponse, error) {
|
||||
return api.Status(vin)
|
||||
}, cache),
|
||||
chargerG: util.Cached(func() (ChargerResponse, error) {
|
||||
return api.Charger(vin)
|
||||
}, cache),
|
||||
// climateG: util.Cached(func() (interface{}, error) {
|
||||
// return api.Climater(vin)
|
||||
// }, cache),
|
||||
settingsG: util.Cached(func() (SettingsResponse, error) {
|
||||
return api.Settings(vin)
|
||||
}, cache),
|
||||
action: func(action, value string) error {
|
||||
return api.Action(vin, action, value)
|
||||
},
|
||||
}
|
||||
return impl
|
||||
}
|
||||
|
||||
var _ api.Battery = (*Provider)(nil)
|
||||
|
||||
// Soc implements the api.Vehicle interface
|
||||
func (v *Provider) Soc() (float64, error) {
|
||||
res, err := v.chargerG()
|
||||
if err == nil {
|
||||
return float64(res.Battery.StateOfChargeInPercent), 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.chargerG()
|
||||
if err == nil {
|
||||
if res.Plug.ConnectionState == "Connected" {
|
||||
status = api.StatusB
|
||||
}
|
||||
if res.Charging.State == "Charging" {
|
||||
status = api.StatusC
|
||||
}
|
||||
}
|
||||
|
||||
return status, err
|
||||
}
|
||||
|
||||
var _ api.VehicleFinishTimer = (*Provider)(nil)
|
||||
|
||||
// FinishTime implements the api.VehicleFinishTimer interface
|
||||
func (v *Provider) FinishTime() (time.Time, error) {
|
||||
res, err := v.chargerG()
|
||||
if err == nil {
|
||||
crg := res.Charging
|
||||
|
||||
// estimate not available
|
||||
if crg.State == "Error" || crg.ChargingType == "Invalid" {
|
||||
return time.Time{}, api.ErrNotAvailable
|
||||
}
|
||||
|
||||
remaining := time.Duration(crg.RemainingToCompleteInSeconds) * time.Second
|
||||
return time.Now().Add(remaining), err
|
||||
}
|
||||
|
||||
return time.Time{}, err
|
||||
}
|
||||
|
||||
var _ api.VehicleRange = (*Provider)(nil)
|
||||
|
||||
// Range implements the api.VehicleRange interface
|
||||
func (v *Provider) Range() (rng int64, err error) {
|
||||
res, err := v.chargerG()
|
||||
return res.Battery.CruisingRangeElectricInMeters / 1e3, err
|
||||
}
|
||||
|
||||
var _ api.VehicleOdometer = (*Provider)(nil)
|
||||
|
||||
// Odometer implements the api.VehicleOdometer interface
|
||||
func (v *Provider) Odometer() (odo float64, err error) {
|
||||
res, err := v.statusG()
|
||||
return res.Remote.MileageInKm, 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.climateG()
|
||||
// err == nil {
|
||||
// state := strings.ToLower(res.Climater.Status.ClimatisationStatusData.ClimatisationState.Content)
|
||||
// active := state != "off" && state != "invalid" && state != "error"
|
||||
|
||||
// targetTemp = res.Climater.Settings.TargetTemperature.Content
|
||||
// outsideTemp = res.Climater.Status.TemperatureStatusData.OutdoorTemperature.Content
|
||||
// if math.IsNaN(outsideTemp) {
|
||||
// outsideTemp = targetTemp // cover "invalid"
|
||||
// }
|
||||
|
||||
// return active, outsideTemp, targetTemp, nil
|
||||
// }
|
||||
|
||||
// return active, outsideTemp, targetTemp, err
|
||||
// }
|
||||
|
||||
var _ api.SocLimiter = (*Provider)(nil)
|
||||
|
||||
// GetLimitSoc implements the api.SocLimiter interface
|
||||
func (v *Provider) GetLimitSoc() (int64, error) {
|
||||
res, err := v.settingsG()
|
||||
if err == nil {
|
||||
return int64(res.TargetStateOfChargeInPercent), nil
|
||||
}
|
||||
|
||||
return 0, err
|
||||
}
|
||||
|
||||
var _ api.ChargeController = (*Provider)(nil)
|
||||
|
||||
// ChargeEnable implements the api.ChargeController interface
|
||||
func (v *Provider) ChargeEnable(enable bool) error {
|
||||
action := map[bool]string{true: ActionChargeStart, false: ActionChargeStop}
|
||||
return v.action(ActionCharge, action[enable])
|
||||
}
|
||||
|
|
@ -1,71 +0,0 @@
|
|||
package skoda
|
||||
|
||||
// VehiclesResponse is the /v3/garage api
|
||||
type VehiclesResponse struct {
|
||||
Vehicles []Vehicle
|
||||
}
|
||||
|
||||
type Vehicle struct {
|
||||
ID, VIN string
|
||||
Name string // user-given name
|
||||
LastUpdatedAt string
|
||||
Specification Specification
|
||||
// Connectivities
|
||||
// Capabilities
|
||||
}
|
||||
|
||||
type Specification struct {
|
||||
Title string
|
||||
Model string
|
||||
ModelYear string
|
||||
Body string
|
||||
SystemCode string
|
||||
SystemModelId string
|
||||
Engine struct {
|
||||
Typ string `json:"type"`
|
||||
PowerInKW int
|
||||
}
|
||||
Battery struct {
|
||||
CapacityInKWh int
|
||||
}
|
||||
Gearbox struct {
|
||||
Typ string `json:"type"`
|
||||
}
|
||||
TrimLevel string
|
||||
ManufacturingDate string
|
||||
MaxChargingPowerInKW int
|
||||
}
|
||||
|
||||
// StatusResponse is the /v2/vehicle-status/<vin> api
|
||||
type StatusResponse struct {
|
||||
Remote struct {
|
||||
MileageInKm float64
|
||||
}
|
||||
}
|
||||
|
||||
// ChargerResponse is the /v1/charging/<vin>/status api
|
||||
type ChargerResponse struct {
|
||||
Plug struct {
|
||||
ConnectionState string // Connected
|
||||
LockState string // Unlocked
|
||||
}
|
||||
Charging struct {
|
||||
State string // Error
|
||||
RemainingToCompleteInSeconds int64
|
||||
ChargingPowerInWatts float64
|
||||
ChargingRateInKilometersPerHour float64
|
||||
ChargingType string // Invalid
|
||||
ChargeMode string // MANUAL
|
||||
}
|
||||
Battery struct {
|
||||
CruisingRangeElectricInMeters int64
|
||||
StateOfChargeInPercent int
|
||||
}
|
||||
}
|
||||
|
||||
// SettingsResponse is the /v1/charging/<vin>/settings api
|
||||
type SettingsResponse struct {
|
||||
AutoUnlockPlugWhenCharged string `json:"autoUnlockPlugWhenCharged"`
|
||||
MaxChargeCurrentAc string `json:"maxChargeCurrentAc"`
|
||||
TargetStateOfChargeInPercent int `json:"targetStateOfChargeInPercent"`
|
||||
}
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
|
||||
"github.com/evcc-io/evcc/util"
|
||||
"github.com/evcc-io/evcc/vehicle/vag"
|
||||
"github.com/evcc-io/evcc/vehicle/vag/tokenrefreshservice"
|
||||
"github.com/evcc-io/evcc/vehicle/vag/vwidentity"
|
||||
)
|
||||
|
||||
func TokenRefreshServiceTokenSource(log *util.Logger, data, q url.Values, user, password string) (vag.TokenSource, error) {
|
||||
q, err := vwidentity.Login(log, q, user, password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
trs := tokenrefreshservice.New(log, data)
|
||||
token, err := trs.Exchange(q)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return trs.TokenSource(token), nil
|
||||
}
|
||||
|
|
@ -1,77 +0,0 @@
|
|||
package tokenrefreshservice
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/evcc-io/evcc/util"
|
||||
"github.com/evcc-io/evcc/util/request"
|
||||
"github.com/evcc-io/evcc/util/urlvalues"
|
||||
"github.com/evcc-io/evcc/vehicle/vag"
|
||||
)
|
||||
|
||||
const (
|
||||
BaseURL = "https://tokenrefreshservice.apps.emea.vwapps.io"
|
||||
CodeExchangeURL = BaseURL + "/exchangeAuthCode"
|
||||
RefreshTokenURL = BaseURL + "/refreshTokens"
|
||||
)
|
||||
|
||||
var _ vag.TokenExchanger = (*Service)(nil)
|
||||
|
||||
type Service struct {
|
||||
*request.Helper
|
||||
data url.Values
|
||||
}
|
||||
|
||||
func New(log *util.Logger, q url.Values) *Service {
|
||||
return &Service{
|
||||
Helper: request.NewHelper(log),
|
||||
data: q,
|
||||
}
|
||||
}
|
||||
|
||||
func (v *Service) Exchange(q url.Values) (*vag.Token, error) {
|
||||
if err := urlvalues.Require(q, "id_token", "code"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
data := url.Values{
|
||||
"auth_code": {q.Get("code")},
|
||||
"id_token": {q.Get("id_token")},
|
||||
}
|
||||
|
||||
urlvalues.Merge(data, v.data, q)
|
||||
|
||||
var res vag.Token
|
||||
|
||||
req, err := request.New(http.MethodPost, CodeExchangeURL, strings.NewReader(data.Encode()), request.URLEncoding)
|
||||
if err == nil {
|
||||
err = v.DoJSON(req, &res)
|
||||
}
|
||||
|
||||
return &res, err
|
||||
}
|
||||
|
||||
func (v *Service) Refresh(token *vag.Token) (*vag.Token, error) {
|
||||
data := url.Values{
|
||||
"grant_type": {"refresh_token"},
|
||||
"refresh_token": {token.RefreshToken},
|
||||
}
|
||||
|
||||
urlvalues.Merge(data, v.data)
|
||||
|
||||
var res vag.Token
|
||||
|
||||
req, err := request.New(http.MethodPost, RefreshTokenURL, strings.NewReader(data.Encode()), request.URLEncoding)
|
||||
if err == nil {
|
||||
err = v.DoJSON(req, &res)
|
||||
}
|
||||
|
||||
return &res, err
|
||||
}
|
||||
|
||||
// TokenSource creates token source. Token is refreshed automatically.
|
||||
func (v *Service) TokenSource(token *vag.Token) vag.TokenSource {
|
||||
return vag.RefreshTokenSource(token, v.Refresh)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue