Skoda: replace obsolete Skoda api with Enyaq (#19127)

This commit is contained in:
andig 2025-02-24 08:50:18 +01:00 • committed by GitHub
parent 4c73f18270
commit 1891a58ce5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 16 additions and 533 deletions

View file

@ -1,4 +1,5 @@
template: enyaq
covers: ["skoda"]
products:
- brand: Skoda
description:

View file

@ -1,11 +0,0 @@
template: skoda
products:
- brand: Skoda
description:
de: Andere
en: Others
params:
- preset: vehicle-base
render: |
type: skoda
{{ include "vehicle-base" . }}

View file

@ -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))
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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)
}

View file

@ -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"},
}

View file

@ -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])
}

View file

@ -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"`
}

View file

@ -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
}

View file

@ -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)
}