diff --git a/templates/definition/vehicle/skoda-enyaq.yaml b/templates/definition/vehicle/skoda-enyaq.yaml index 90617239e..53b22c960 100644 --- a/templates/definition/vehicle/skoda-enyaq.yaml +++ b/templates/definition/vehicle/skoda-enyaq.yaml @@ -1,4 +1,5 @@ template: enyaq +covers: ["skoda"] products: - brand: Skoda description: diff --git a/templates/definition/vehicle/skoda.yaml b/templates/definition/vehicle/skoda.yaml deleted file mode 100644 index 0cd5d7a4f..000000000 --- a/templates/definition/vehicle/skoda.yaml +++ /dev/null @@ -1,11 +0,0 @@ -template: skoda -products: - - brand: Skoda - description: - de: Andere - en: Others -params: - - preset: vehicle-base -render: | - type: skoda - {{ include "vehicle-base" . }} diff --git a/util/request/functions.go b/util/request/functions.go index 9a542ad6a..e6583e094 100644 --- a/util/request/functions.go +++ b/util/request/functions.go @@ -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)) } diff --git a/vehicle/skoda.go b/vehicle/skoda.go deleted file mode 100644 index 3b48649ee..000000000 --- a/vehicle/skoda.go +++ /dev/null @@ -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 -} diff --git a/vehicle/skoda/api.go b/vehicle/skoda/api.go deleted file mode 100644 index a60de8b1d..000000000 --- a/vehicle/skoda/api.go +++ /dev/null @@ -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/ 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//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//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 -} diff --git a/vehicle/skoda/myskoda/api.go b/vehicle/skoda/myskoda/api.go index 61574f94e..517545f20 100644 --- a/vehicle/skoda/myskoda/api.go +++ b/vehicle/skoda/myskoda/api.go @@ -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 } diff --git a/vehicle/skoda/myskoda/tokenrefreshservice/endpoint.go b/vehicle/skoda/myskoda/tokenrefreshservice/endpoint.go index 67929b932..ecc406ff8 100644 --- a/vehicle/skoda/myskoda/tokenrefreshservice/endpoint.go +++ b/vehicle/skoda/myskoda/tokenrefreshservice/endpoint.go @@ -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) } diff --git a/vehicle/skoda/params.go b/vehicle/skoda/params.go deleted file mode 100644 index 187a83502..000000000 --- a/vehicle/skoda/params.go +++ /dev/null @@ -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"}, -} diff --git a/vehicle/skoda/provider.go b/vehicle/skoda/provider.go deleted file mode 100644 index c85c4a08d..000000000 --- a/vehicle/skoda/provider.go +++ /dev/null @@ -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]) -} diff --git a/vehicle/skoda/types.go b/vehicle/skoda/types.go deleted file mode 100644 index f020b5cb2..000000000 --- a/vehicle/skoda/types.go +++ /dev/null @@ -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/ api -type StatusResponse struct { - Remote struct { - MileageInKm float64 - } -} - -// ChargerResponse is the /v1/charging//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//settings api -type SettingsResponse struct { - AutoUnlockPlugWhenCharged string `json:"autoUnlockPlugWhenCharged"` - MaxChargeCurrentAc string `json:"maxChargeCurrentAc"` - TargetStateOfChargeInPercent int `json:"targetStateOfChargeInPercent"` -} diff --git a/vehicle/vag/service/tokenrefreshservice.go b/vehicle/vag/service/tokenrefreshservice.go deleted file mode 100644 index e818b936c..000000000 --- a/vehicle/vag/service/tokenrefreshservice.go +++ /dev/null @@ -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 -} diff --git a/vehicle/vag/tokenrefreshservice/endpoint.go b/vehicle/vag/tokenrefreshservice/endpoint.go deleted file mode 100644 index ecd5b5f34..000000000 --- a/vehicle/vag/tokenrefreshservice/endpoint.go +++ /dev/null @@ -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) -}