Move MySkoda implementation into own package (#14349)
Some checks failed
Release / Hassio Addon (push) Has been cancelled
Release / call-build-workflow (push) Has been cancelled
Release / Publish Docker :release (push) Has been cancelled
Release / Github & APT (push) Has been cancelled
Release / Demo (push) Has been cancelled

This commit is contained in:
Michael Heß 2024-06-13 22:10:07 +02:00 • committed by GitHub
parent 85b14d383b
commit 9ff704e763
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 452 additions and 107 deletions

View file

@ -6,8 +6,8 @@ import (
"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/skoda/service"
"github.com/evcc-io/evcc/vehicle/skoda/myskoda"
"github.com/evcc-io/evcc/vehicle/skoda/myskoda/service"
)
// https://gitlab.com/prior99/skoda
@ -15,7 +15,7 @@ import (
// Enyaq is an api.Vehicle implementation for Skoda Enyaq cars
type Enyaq struct {
*embed
*skoda.Provider // provides the api implementations
*myskoda.Provider // provides the api implementations
}
func init() {
@ -50,17 +50,17 @@ func NewEnyaqFromConfig(other map[string]interface{}) (api.Vehicle, error) {
log := util.NewLogger("enyaq").Redact(cc.User, cc.Password, cc.VIN)
// use Skoda api to resolve list of vehicles
ts, err := service.TokenRefreshServiceTokenSource(log, skoda.TRSParams, skoda.AuthParams, cc.User, cc.Password)
ts, err := service.TokenRefreshServiceTokenSource(log, myskoda.TRSParams, myskoda.AuthParams, cc.User, cc.Password)
if err != nil {
return nil, err
}
api := skoda.NewAPI(log, ts)
api := myskoda.NewAPI(log, ts)
api.Client.Timeout = cc.Timeout
vehicle, err := ensureVehicleEx(
cc.VIN, api.Vehicles,
func(v skoda.Vehicle) (string, error) {
func(v myskoda.Vehicle) (string, error) {
return v.VIN, nil
},
)
@ -75,10 +75,10 @@ func NewEnyaqFromConfig(other map[string]interface{}) (api.Vehicle, error) {
// reuse tokenService to build provider
if err == nil {
api := skoda.NewAPI(log, ts)
api := myskoda.NewAPI(log, ts)
api.Client.Timeout = cc.Timeout
v.Provider = skoda.NewProvider(api, vehicle.VIN, cc.Cache)
v.Provider = myskoda.NewProvider(api, vehicle.VIN, cc.Cache)
}
return v, err

View file

@ -9,7 +9,7 @@ import (
"golang.org/x/oauth2"
)
const BaseURI = "https://mysmob.api.connect.skoda-auto.cz/api"
const BaseURI = "https://api.connect.skoda-auto.cz/api"
// API is the Skoda api client
type API struct {
@ -30,35 +30,28 @@ func NewAPI(log *util.Logger, ts oauth2.TokenSource) *API {
return v
}
// Vehicles implements the /v2/garage response
// Vehicles implements the /v3/garage response
func (v *API) Vehicles() ([]Vehicle, error) {
var res VehiclesResponse
uri := fmt.Sprintf("%s/v2/garage", BaseURI)
uri := fmt.Sprintf("%s/v3/garage", BaseURI)
err := v.GetJSON(uri, &res)
return res.Vehicles, err
}
func (v *API) VehicleDetails(vin string) (Vehicle, error) {
var res Vehicle
uri := fmt.Sprintf("%s/v2/garage/vehicles/%s", BaseURI, vin)
err := v.GetJSON(uri, &res)
return res, err
}
// Status implements the /v2/vehicle-status/<vin> response
func (v *API) Status(vin string) (StatusResponse, error) {
var res StatusResponse
uri := fmt.Sprintf("%s/v1/vehicle-health-report/warning-lights/%s", BaseURI, vin)
uri := fmt.Sprintf("%s/v2/vehicle-status/%s", BaseURI, vin)
err := v.GetJSON(uri, &res)
return res, err
}
// Charger implements the /v1/charging/<vin> response
// 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", BaseURI, vin)
uri := fmt.Sprintf("%s/v1/charging/%s/status", BaseURI, vin)
err := v.GetJSON(uri, &res)
return res, err
}
@ -66,48 +59,33 @@ func (v *API) Charger(vin string) (ChargerResponse, error) {
// Settings implements the /v1/charging/<vin>/settings response
func (v *API) Settings(vin string) (SettingsResponse, error) {
var res SettingsResponse
chrgRes, err := v.Charger(vin)
if err == nil {
res = chrgRes.Settings
}
return res, err
}
// Climater implements the /v2/air-conditioning/<vin> response
func (v *API) Climater(vin string) (ClimaterResponse, error) {
var res ClimaterResponse
uri := fmt.Sprintf("%s/v2/air-conditioning/%s", BaseURI, vin)
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"
ActionChargeStart = "Start"
ActionChargeStop = "Stop"
)
// Action executes a vehicle action
func (v *API) Action(vin, action, value string) error {
// @POST("api/v1/charging/{vin}/start")
// @POST("api/v1/charging/{vin}/stop")
uri := fmt.Sprintf("%s/v1/%s/%s/%s", BaseURI, action, vin, value)
var res map[string]interface{}
uri := fmt.Sprintf("%s/v1/%s/operation-requests?vin=%s", BaseURI, action, vin)
req, err := request.New(http.MethodPost, uri, nil, request.JSONEncoding)
if err == nil {
err = v.DoJSON(req, nil)
}
return err
}
func (v *API) WakeUp(vin string) error {
// @POST("api/v1/vehicle-wakeup/{vin}")
uri := fmt.Sprintf("%s/v1/vehicle-wakeup/%s", BaseURI, vin)
req, err := request.New(http.MethodPost, uri, nil, request.JSONEncoding)
if err == nil {
err = v.DoJSON(req, nil)
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

@ -0,0 +1,13 @@
package connect
import (
"net/url"
)
// Skoda connect api
var AuthParams = url.Values{
"response_type": {"code id_token"},
"redirect_uri": {"skodaconnect://oidc.login/"},
"client_id": {"7f045eee-7003-4379-9968-9355ed2adb06@apps_vw-dilab_com"},
"scope": {"openid profile mbb mileage"}, // phone address cars email birthdate badge dealers driversLicense
}

View file

@ -0,0 +1,113 @@
package myskoda
import (
"fmt"
"net/http"
"github.com/evcc-io/evcc/util"
"github.com/evcc-io/evcc/util/request"
"golang.org/x/oauth2"
)
const BaseURI = "https://mysmob.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 /v2/garage response
func (v *API) Vehicles() ([]Vehicle, error) {
var res VehiclesResponse
uri := fmt.Sprintf("%s/v2/garage", BaseURI)
err := v.GetJSON(uri, &res)
return res.Vehicles, err
}
func (v *API) VehicleDetails(vin string) (Vehicle, error) {
var res Vehicle
uri := fmt.Sprintf("%s/v2/garage/vehicles/%s", BaseURI, vin)
err := v.GetJSON(uri, &res)
return res, err
}
// Status implements the /v2/vehicle-status/<vin> response
func (v *API) Status(vin string) (StatusResponse, error) {
var res StatusResponse
uri := fmt.Sprintf("%s/v1/vehicle-health-report/warning-lights/%s", BaseURI, vin)
err := v.GetJSON(uri, &res)
return res, err
}
// Charger implements the /v1/charging/<vin> response
func (v *API) Charger(vin string) (ChargerResponse, error) {
var res ChargerResponse
uri := fmt.Sprintf("%s/v1/charging/%s", 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
chrgRes, err := v.Charger(vin)
if err == nil {
res = chrgRes.Settings
}
return res, err
}
// Climater implements the /v2/air-conditioning/<vin> response
func (v *API) Climater(vin string) (ClimaterResponse, error) {
var res ClimaterResponse
uri := fmt.Sprintf("%s/v2/air-conditioning/%s", 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 {
// @POST("api/v1/charging/{vin}/start")
// @POST("api/v1/charging/{vin}/stop")
uri := fmt.Sprintf("%s/v1/%s/%s/%s", BaseURI, action, vin, value)
req, err := request.New(http.MethodPost, uri, nil, request.JSONEncoding)
if err == nil {
err = v.DoJSON(req, nil)
}
return err
}
func (v *API) WakeUp(vin string) error {
// @POST("api/v1/vehicle-wakeup/{vin}")
uri := fmt.Sprintf("%s/v1/vehicle-wakeup/%s", BaseURI, vin)
req, err := request.New(http.MethodPost, uri, nil, request.JSONEncoding)
if err == nil {
err = v.DoJSON(req, nil)
}
return err
}

View file

@ -0,0 +1,24 @@
package myskoda
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": {"7f045eee-7003-4379-9968-9355ed2adb06@apps_vw-dilab_com"},
"redirect_uri": {"myskoda://redirect/login/"},
"scope": {"address badge birthdate cars driversLicense dealers email mileage mbb nationalIdentifier openid phone profession profile vin"},
}
// TokenRefreshService parameters
var TRSParams = url.Values{
"brand": {"skoda"},
}

View file

@ -0,0 +1,162 @@
package myskoda
import (
"time"
"github.com/evcc-io/evcc/api"
"github.com/evcc-io/evcc/provider"
)
// Provider implements the vehicle api
type Provider struct {
statusG func() (StatusResponse, error)
chargerG func() (ChargerResponse, error)
settingsG func() (SettingsResponse, error)
climateG func() (ClimaterResponse, error)
action func(action, value string) error
wakeup func() error
}
// NewProvider creates a vehicle api provider
func NewProvider(api *API, vin string, cache time.Duration) *Provider {
impl := &Provider{
statusG: provider.Cached(func() (StatusResponse, error) {
return api.Status(vin)
}, cache),
chargerG: provider.Cached(func() (ChargerResponse, error) {
return api.Charger(vin)
}, cache),
climateG: provider.Cached(func() (ClimaterResponse, error) {
return api.Climater(vin)
}, cache),
settingsG: provider.Cached(func() (SettingsResponse, error) {
return api.Settings(vin)
}, cache),
action: func(action, value string) error {
return api.Action(vin, action, value)
},
wakeup: func() error {
return api.WakeUp(vin)
},
}
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.Status.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.climateG()
if err == nil {
if res.ChargerConnectionState == "CONNECTED" {
status = api.StatusB
}
}
resChrg, err := v.chargerG()
if err == nil {
if resChrg.Status.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.Status
// estimate not available
if crg.State == "Error" || crg.ChargeType == "Invalid" {
return time.Time{}, api.ErrNotAvailable
}
remaining := time.Duration(crg.RemainingTimeToFullyChargedInMinutes) * time.Minute
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.Status.Battery.RemainingCruisingRangeInMeters / 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.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.chargerG()
if err == nil {
return int64(res.Settings.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])
}
var _ api.Resurrector = (*Provider)(nil)
// WakeUp implements the api.Resurrector interface
func (v *Provider) WakeUp() error {
return v.wakeup()
}

View file

@ -4,7 +4,7 @@ import (
"net/url"
"github.com/evcc-io/evcc/util"
"github.com/evcc-io/evcc/vehicle/skoda/tokenrefreshservice"
"github.com/evcc-io/evcc/vehicle/skoda/myskoda/tokenrefreshservice"
"github.com/evcc-io/evcc/vehicle/vag"
"github.com/evcc-io/evcc/vehicle/vag/vwidentity"
)

View file

@ -0,0 +1,73 @@
package myskoda
// 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 {
MileageInKm float64
}
// ChargerResponse is the /v2/charging/<vin> api
type ChargerResponse struct {
IsVehicleInSaveLocation bool
Status struct {
ChargingRateInKilometersPerHour float64
ChargePowerInKw float64
RemainingTimeToFullyChargedInMinutes int64
State string
ChargeType string
Battery struct {
RemainingCruisingRangeInMeters int64
StateOfChargeInPercent int
}
}
Settings SettingsResponse
}
// SettingsResponse is the /v1/charging/<vin>/settings api
type SettingsResponse struct {
AutoUnlockPlugWhenCharged string `json:"autoUnlockPlugWhenCharged"`
MaxChargeCurrentAc string `json:"maxChargeCurrentAc"`
TargetStateOfChargeInPercent int `json:"targetStateOfChargeInPercent"`
}
// ChargerResponse is the /v2/air-conditioning/<vin> api
type ClimaterResponse struct {
State string `json:"state"`
ChargerConnectionState string `json:"chargerConnectionState"`
ChargerLockState string `json:"chargerLockState"`
}

View file

@ -13,9 +13,9 @@ const (
// Skoda native api
var AuthParams = url.Values{
"response_type": {"code id_token"},
"client_id": {"7f045eee-7003-4379-9968-9355ed2adb06@apps_vw-dilab_com"},
"redirect_uri": {"myskoda://redirect/login/"},
"scope": {"address badge birthdate cars driversLicense dealers email mileage mbb nationalIdentifier openid phone profession profile vin"},
"client_id": {"f9a2359a-b776-46d9-bd0c-db1904343117@apps_vw-dilab_com"},
"redirect_uri": {"skodaconnect://oidc.login/"},
"scope": {"openid mbb profile"},
}
// TokenRefreshService parameters

View file

@ -12,9 +12,7 @@ type Provider struct {
statusG func() (StatusResponse, error)
chargerG func() (ChargerResponse, error)
settingsG func() (SettingsResponse, error)
climateG func() (ClimaterResponse, error)
action func(action, value string) error
wakeup func() error
}
// NewProvider creates a vehicle api provider
@ -26,18 +24,15 @@ func NewProvider(api *API, vin string, cache time.Duration) *Provider {
chargerG: provider.Cached(func() (ChargerResponse, error) {
return api.Charger(vin)
}, cache),
climateG: provider.Cached(func() (ClimaterResponse, error) {
return api.Climater(vin)
}, cache),
// climateG: provider.Cached(func() (interface{}, error) {
// return api.Climater(vin)
// }, cache),
settingsG: provider.Cached(func() (SettingsResponse, error) {
return api.Settings(vin)
}, cache),
action: func(action, value string) error {
return api.Action(vin, action, value)
},
wakeup: func() error {
return api.WakeUp(vin)
},
}
return impl
}
@ -48,7 +43,7 @@ var _ api.Battery = (*Provider)(nil)
func (v *Provider) Soc() (float64, error) {
res, err := v.chargerG()
if err == nil {
return float64(res.Status.Battery.StateOfChargeInPercent), nil
return float64(res.Battery.StateOfChargeInPercent), nil
}
return 0, err
@ -60,16 +55,12 @@ var _ api.ChargeState = (*Provider)(nil)
func (v *Provider) Status() (api.ChargeStatus, error) {
status := api.StatusA // disconnected
res, err := v.climateG()
res, err := v.chargerG()
if err == nil {
if res.ChargerConnectionState == "CONNECTED" {
if res.Plug.ConnectionState == "Connected" {
status = api.StatusB
}
}
resChrg, err := v.chargerG()
if err == nil {
if resChrg.Status.State == "CHARGING" {
if res.Charging.State == "Charging" {
status = api.StatusC
}
}
@ -83,14 +74,14 @@ var _ api.VehicleFinishTimer = (*Provider)(nil)
func (v *Provider) FinishTime() (time.Time, error) {
res, err := v.chargerG()
if err == nil {
crg := res.Status
crg := res.Charging
// estimate not available
if crg.State == "Error" || crg.ChargeType == "Invalid" {
if crg.State == "Error" || crg.ChargingType == "Invalid" {
return time.Time{}, api.ErrNotAvailable
}
remaining := time.Duration(crg.RemainingTimeToFullyChargedInMinutes) * time.Minute
remaining := time.Duration(crg.RemainingToCompleteInSeconds) * time.Second
return time.Now().Add(remaining), err
}
@ -102,7 +93,7 @@ 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.Status.Battery.RemainingCruisingRangeInMeters / 1e3, err
return res.Battery.CruisingRangeElectricInMeters / 1e3, err
}
var _ api.VehicleOdometer = (*Provider)(nil)
@ -110,7 +101,7 @@ 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.MileageInKm, err
return res.Remote.MileageInKm, err
}
// var _ api.VehicleClimater = (*Provider)(nil)
@ -138,9 +129,9 @@ var _ api.SocLimiter = (*Provider)(nil)
// GetLimitSoc implements the api.SocLimiter interface
func (v *Provider) GetLimitSoc() (int64, error) {
res, err := v.chargerG()
res, err := v.settingsG()
if err == nil {
return int64(res.Settings.TargetStateOfChargeInPercent), nil
return int64(res.TargetStateOfChargeInPercent), nil
}
return 0, err
@ -153,10 +144,3 @@ func (v *Provider) ChargeEnable(enable bool) error {
action := map[bool]string{true: ActionChargeStart, false: ActionChargeStop}
return v.action(ActionCharge, action[enable])
}
var _ api.Resurrector = (*Provider)(nil)
// WakeUp implements the api.Resurrector interface
func (v *Provider) WakeUp() error {
return v.wakeup()
}

View file

@ -38,24 +38,29 @@ type Specification struct {
// StatusResponse is the /v2/vehicle-status/<vin> api
type StatusResponse struct {
MileageInKm float64
Remote struct {
MileageInKm float64
}
}
// ChargerResponse is the /v2/charging/<vin> api
// ChargerResponse is the /v1/charging/<vin>/status api
type ChargerResponse struct {
IsVehicleInSaveLocation bool
Status struct {
ChargingRateInKilometersPerHour float64
ChargePowerInKw float64
RemainingTimeToFullyChargedInMinutes int64
State string
ChargeType string
Battery struct {
RemainingCruisingRangeInMeters int64
StateOfChargeInPercent int
}
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
}
Settings SettingsResponse
}
// SettingsResponse is the /v1/charging/<vin>/settings api
@ -64,10 +69,3 @@ type SettingsResponse struct {
MaxChargeCurrentAc string `json:"maxChargeCurrentAc"`
TargetStateOfChargeInPercent int `json:"targetStateOfChargeInPercent"`
}
// ChargerResponse is the /v2/air-conditioning/<vin> api
type ClimaterResponse struct {
State string `json:"state"`
ChargerConnectionState string `json:"chargerConnectionState"`
ChargerLockState string `json:"chargerLockState"`
}