diff --git a/charger/ocpp/forwarder.go b/charger/ocpp/forwarder.go index 7f5e4d0d6..16820671c 100644 --- a/charger/ocpp/forwarder.go +++ b/charger/ocpp/forwarder.go @@ -170,9 +170,6 @@ func ForwarderRules() []ForwarderRule { return forwarderRules } -// StartForwarder is a no-op; hooks fire on every charger connection. -func StartForwarder() {} - // actionsRelayedToUpstream lists actions for which upstream is the authoritative // Central System: evcc's handler is bypassed and upstream's reply relayed back. var actionsRelayedToUpstream = map[string]bool{ diff --git a/util/oauth/helper.go b/util/oauth/helper.go deleted file mode 100644 index 253350974..000000000 --- a/util/oauth/helper.go +++ /dev/null @@ -1,51 +0,0 @@ -package oauth - -import ( - "time" - - "github.com/evcc-io/evcc/util" - "golang.org/x/oauth2" -) - -// Refresh refreshes the token every 5m. If token refresh fails 5 times, it is aborted. -func Refresh(log *util.Logger, token *oauth2.Token, ts oauth2.TokenSource, optMaxTokenLifetime ...time.Duration) { - var failed int - - // limit lifetime of initial token - limitTokenLife(token, optMaxTokenLifetime...) - - for range time.Tick(5 * time.Minute) { - // get token- either previous or new - t, err := ts.Token() - if err != nil { - // error means refresh failed - failed++ - if failed > 5 { - log.ERROR.Printf("token refresh: %v, giving up", err) - return - } - - log.ERROR.Printf("token refresh: %v", err) - continue - } - - failed = 0 - - // limit lifetime of new tokens - if t.Expiry != token.Expiry { - token = t - limitTokenLife(token, optMaxTokenLifetime...) - } - } -} - -func limitTokenLife(token *oauth2.Token, optMaxTokenLifetime ...time.Duration) { - if len(optMaxTokenLifetime) != 1 { - return - } - - maxTokenLifetime := optMaxTokenLifetime[0] - if time.Until(token.Expiry) > maxTokenLifetime { - token.Expiry = time.Now().Add(maxTokenLifetime) - } -} diff --git a/util/urlvalues/url.go b/util/urlvalues/url.go index 27e07654d..542663ac4 100644 --- a/util/urlvalues/url.go +++ b/util/urlvalues/url.go @@ -16,15 +16,6 @@ func Copy(q url.Values) url.Values { return res } -// Merge copies multiple from url values into to -func Merge(to url.Values, from ...url.Values) { - for _, vv := range from { - for k, v := range vv { - to[k] = append(to[k], v...) - } - } -} - // Require verifies that url contains the required non-nil values func Require(q url.Values, keys ...string) error { for _, k := range keys { diff --git a/vehicle/mb/identity.go b/vehicle/mb/identity.go deleted file mode 100644 index 5627e5ce2..000000000 --- a/vehicle/mb/identity.go +++ /dev/null @@ -1,132 +0,0 @@ -package mb - -import ( - "context" - "fmt" - "net/http" - "net/http/cookiejar" - "net/url" - "strings" - - "github.com/evcc-io/evcc/util" - "github.com/evcc-io/evcc/util/request" - "github.com/samber/lo" - "golang.org/x/net/publicsuffix" - "golang.org/x/oauth2" -) - -// https://github.com/TA2k/ioBroker.smart-eq - -// https://id.mercedes-benz.com/.well-known/openid-configuration -const OAuthURI = "https://id.mercedes-benz.com" - -type Identity struct { - *request.Helper - oc *oauth2.Config - oauth2.TokenSource -} - -// NewIdentity creates Mercedes Benz identity -func NewIdentity(log *util.Logger, oc *oauth2.Config) *Identity { - return &Identity{ - Helper: request.NewHelper(log), - oc: oc, - } -} - -func (v *Identity) Login(user, password string) error { - if v.Client.Jar == nil { - v.Client.Jar, _ = cookiejar.New(&cookiejar.Options{ - PublicSuffixList: publicsuffix.List, - }) - } - - var param request.InterceptResult - v.Client.CheckRedirect, param = request.InterceptRedirect("resume", false) - - cv := oauth2.GenerateVerifier() - - state := lo.RandomString(16, lo.AlphanumericCharset) - uri := v.oc.AuthCodeURL(state, oauth2.AccessTypeOffline, oauth2.S256ChallengeOption(cv)) - if _, err := v.Get(uri); err != nil { - return err - } - - resume, err := param() - if err != nil { - return err - } - - v.Client.CheckRedirect = nil - - data := struct { - Username string `json:"username"` - Password string `json:"password,omitempty"` - RememberMe bool `json:"rememberMe,omitempty"` - }{ - Username: user, - } - - var res struct { - Result, Token string - Errors []struct{ Key string } - } - - uri = fmt.Sprintf("%s/ciam/auth/login/user", OAuthURI) - req, err := request.New(http.MethodPost, uri, request.MarshalJSON(data), request.JSONEncoding) - if err == nil { - if err = v.DoJSON(req, &res); err != nil && len(res.Errors) > 0 { - err = fmt.Errorf("%s: %w", res.Errors[0].Key, err) - } - } - - if err == nil { - data.Password = password - data.RememberMe = true - - uri = fmt.Sprintf("%s/ciam/auth/login/pass", OAuthURI) - req, err = request.New(http.MethodPost, uri, request.MarshalJSON(data), request.JSONEncoding) - if err == nil { - if err = v.DoJSON(req, &res); err != nil && len(res.Errors) > 0 { - err = fmt.Errorf("%s: %w", res.Errors[0].Key, err) - } - } - } - - if err == nil && res.Token == "" && res.Result != "" { - err = fmt.Errorf("missing token: %s", res.Result) - } - - var code string - if err == nil { - params := url.Values{ - "token": {res.Token}, - } - - var param request.InterceptResult - v.Client.CheckRedirect, param = request.InterceptRedirect("code", true) - - uri := OAuthURI + resume - if _, err = v.Post(uri, request.FormContent, strings.NewReader(params.Encode())); err == nil { - code, err = param() - } - - v.Client.CheckRedirect = nil - } - - var token *oauth2.Token - if err == nil { - ctx, cancel := context.WithTimeout( - context.WithValue(context.Background(), oauth2.HTTPClient, v.Client), - request.Timeout) - defer cancel() - - token, err = v.oc.Exchange(ctx, code, oauth2.VerifierOption(cv)) - } - - if err == nil { - v.TokenSource = v.oc.TokenSource(context.Background(), token) - } - - return err -} diff --git a/vehicle/mercedes/types.go b/vehicle/mercedes/types.go index 1875d88e3..57a33f1d2 100644 --- a/vehicle/mercedes/types.go +++ b/vehicle/mercedes/types.go @@ -1,8 +1,6 @@ package mercedes import ( - "fmt" - "html" "time" ) @@ -12,19 +10,6 @@ var Regions = map[string]string{ "noram": "North-America", } -type ErrorInfo struct { - ErrorCode int - ErrorMessage string - ErrorDetails string -} - -func (e ErrorInfo) Error() error { - if e.ErrorCode == 0 { - return nil - } - return fmt.Errorf("%s: %s", e.ErrorMessage, html.UnescapeString(e.ErrorDetails)) -} - type PinRequest struct { EmailOrPhoneNumber string `json:"emailOrPhoneNumber"` CountryCode string `json:"countryCode"` diff --git a/vehicle/smart/api.go b/vehicle/smart/api.go deleted file mode 100644 index 30f08e907..000000000 --- a/vehicle/smart/api.go +++ /dev/null @@ -1,102 +0,0 @@ -package smart - -import ( - "fmt" - - "github.com/evcc-io/evcc/util" - "github.com/evcc-io/evcc/util/request" - "github.com/evcc-io/evcc/util/transport" - "github.com/evcc-io/evcc/vehicle/mb" - "github.com/samber/lo" - "golang.org/x/oauth2" -) - -// https://github.com/TA2k/ioBroker.smart-eq - -const ApiURI = "https://oneapp.microservice.smart.mercedes-benz.com/seqc/v0" - -var OAuth2Config = &oauth2.Config{ - ClientID: "70d89501-938c-4bec-82d0-6abb550b0825", - RedirectURL: "https://oneapp.microservice.smart.mercedes-benz.com", - Endpoint: oauth2.Endpoint{ - AuthURL: mb.OAuthURI + "/as/authorization.oauth2", - TokenURL: mb.OAuthURI + "/as/token.oauth2", - }, - Scopes: []string{"openid", "profile", "email", "phone", "ciam-uid", "offline_access"}, -} - -type API struct { - *request.Helper -} - -func NewAPI(log *util.Logger, identity oauth2.TokenSource) *API { - v := &API{ - Helper: request.NewHelper(log), - } - - // replace client transport with authenticated transport - v.Client.Transport = &transport.Decorator{ - Base: &oauth2.Transport{ - Source: identity, - Base: v.Client.Transport, - }, - Decorator: transport.DecorateHeaders(map[string]string{ - "accept": "*/*", - "accept-language": "de-DE;q=1.0", - "guid": "280C6B55-F179-4428-88B6-E0CCF5C22A7C", - "x-applicationname": OAuth2Config.ClientID, - }), - } - - return v -} - -func (v *API) Vehicles() ([]string, error) { - type vehicle struct { - FIN string - } - - var res struct { - Authorizations, LicensePlates []vehicle - Error string - ErrorDescription string `json:"error_description"` - } - - uri := fmt.Sprintf("%s/users/current", ApiURI) - err := v.GetJSON(uri, &res) - if err != nil && res.Error != "" { - err = fmt.Errorf("%s (%s): %w", res.Error, res.ErrorDescription, err) - } - - vehicles := lo.Map(res.LicensePlates, func(v vehicle, _ int) string { - return v.FIN - }) - - return vehicles, err -} - -func (v *API) Status(vin string) (StatusResponse, error) { - var res StatusResponse - - uri := fmt.Sprintf("%s/vehicles/%s/init-data?requestedData=BOTH&countryCode=DE&locale=de-DE", ApiURI, vin) - err := v.GetJSON(uri, &res) - - if err != nil && res.Error != "" { - err = fmt.Errorf("%s (%s): %w", res.Error, res.ErrorDescription, err) - } - - return res, err -} - -func (v *API) Refresh(vin string) (StatusResponse, error) { - var res StatusResponse - - uri := fmt.Sprintf("%s/vehicles/%s/refresh-data", ApiURI, vin) - err := v.GetJSON(uri, &res) - - if err != nil && res.Error != "" { - err = fmt.Errorf("%s (%s): %w", res.Error, res.ErrorDescription, err) - } - - return res, err -} diff --git a/vehicle/smart/provider.go b/vehicle/smart/provider.go deleted file mode 100644 index e974b314c..000000000 --- a/vehicle/smart/provider.go +++ /dev/null @@ -1,99 +0,0 @@ -package smart - -import ( - "fmt" - "time" - - "github.com/evcc-io/evcc/api" - "github.com/evcc-io/evcc/util" -) - -// https://github.com/TA2k/ioBroker.smart-eq - -type Provider struct { - statusG func() (StatusResponse, error) - expiry time.Duration -} - -func NewProvider(log *util.Logger, api *API, vin string, expiry, cache time.Duration) *Provider { - v := &Provider{ - expiry: expiry, - } - - v.statusG = util.Cached(func() (StatusResponse, error) { - return v.status( - func() (StatusResponse, error) { return api.Status(vin) }, - func() (StatusResponse, error) { return api.Refresh(vin) }, - ) - }, cache) - - return v -} - -func (v *Provider) status(statusG, refreshG func() (StatusResponse, error)) (StatusResponse, error) { - res, err := statusG() - - // if ts := res.Status.Data.Soc.Ts.Time; err == nil && ts.Add(v.expiry).Before(time.Now()) { - // fmt.Println("--------------------------", ts) - // res, err = refreshG() - // ts := res.Status.Data.Soc.Ts.Time - // fmt.Println("--------------------------", ts) - // } - - return res, err -} - -// Soc implements the api.Vehicle interface -func (v *Provider) Soc() (float64, error) { - res, err := v.statusG() - return res.Status.Data.Soc.Value, err -} - -var _ api.ChargeState = (*Provider)(nil) - -// Range implements the api.VehicleRange interface -func (v *Provider) Status() (api.ChargeStatus, error) { - res, err := v.statusG() - if err != nil { - return api.StatusNone, err - } - - cs := res.PreCond.Data.ChargingStatus - if cs.Status != 0 { - return api.StatusNone, fmt.Errorf("unknown status/value: %d/%d", cs.Status, cs.Value) - } - - // confirmed status/value/active combinations (https://github.com/evcc-io/evcc/discussions/5596#discussioncomment-4556035) - // 0/0/active: charging - // 0/2/*: connected - // 0/3/*: disconnected - switch cs.Value { - case 0: - if res.PreCond.Data.ChargingActive.Value { - return api.StatusC, nil - } - return api.StatusB, nil - case 1, 2: - return api.StatusB, nil - case 3: - return api.StatusA, nil - default: - return api.StatusNone, fmt.Errorf("unknown status/value: %d/%d", cs.Status, cs.Value) - } -} - -var _ api.VehicleRange = (*Provider)(nil) - -// Range implements the api.VehicleRange interface -func (v *Provider) Range() (int64, error) { - res, err := v.statusG() - return int64(res.Status.Data.RangeElectric.Value), err -} - -var _ api.VehicleOdometer = (*Provider)(nil) - -// Odometer implements the api.VehicleOdometer interface -func (v *Provider) Odometer() (float64, error) { - res, err := v.statusG() - return res.Status.Data.Odo.Value, err -} diff --git a/vehicle/smart/types.go b/vehicle/smart/types.go deleted file mode 100644 index 0b823f5e7..000000000 --- a/vehicle/smart/types.go +++ /dev/null @@ -1,61 +0,0 @@ -package smart - -import ( - "strconv" - "time" -) - -type StatusResponse struct { - PreCond struct { - Data struct { - ChargingPower FloatValue - ChargingActive BoolValue - ChargingStatus IntValue - } `json:"data"` - } - Status struct { - Data struct { - Odo FloatValue - RangeElectric FloatValue - Soc FloatValue - } `json:"data"` - } - Images []string - Error string - ErrorDescription string `json:"error_description"` -} - -type BoolValue struct { - Status int - Value bool - Ts TimeSecs -} - -type IntValue struct { - Status int - Value int - Ts TimeSecs -} - -type FloatValue struct { - Status int - Value float64 - Ts TimeSecs -} - -// TimeSecs implements JSON unmarshal for Unix timestamps in seconds -type TimeSecs struct { - time.Time -} - -// UnmarshalJSON decodes unix timestamps in ms into time.Time -func (ct *TimeSecs) UnmarshalJSON(data []byte) error { - i, err := strconv.ParseInt(string(data), 10, 64) - - if err == nil { - t := time.Unix(i, 0) - ct.Time = t - } - - return err -} diff --git a/vehicle/subaru/api.go b/vehicle/subaru/api.go index 9ba15dcad..ee2f9602e 100644 --- a/vehicle/subaru/api.go +++ b/vehicle/subaru/api.go @@ -70,10 +70,6 @@ func NewAPI(log *util.Logger, identity *Identity) *API { return v } -func (v *API) IDToken() string { - return v.identity.IDToken() -} - func (v *API) Vehicles() ([]string, error) { uri := fmt.Sprintf("%s/%s", ApiBaseUrl, VehicleGuidPath) diff --git a/vehicle/vag/tokensource.go b/vehicle/vag/tokensource.go index 3666c2e50..a6d19d2e2 100644 --- a/vehicle/vag/tokensource.go +++ b/vehicle/vag/tokensource.go @@ -67,62 +67,3 @@ func (ts *tokenSource) TokenEx() (*Token, error) { func (ts *tokenSource) mergeToken(t *Token) error { return mergo.Merge(ts.token, t, mergo.WithOverride) } - -type metaTokenSource struct { - mu sync.Mutex - ts TokenSource - newT func() (*Token, error) - newTS func(*Token) TokenSource -} - -// MetaTokenSource creates a token source that is created using the -// `newTS` function or recreated once it fails to return tokens. -// The recreation uses a new bootstrap token provided by the `newT` function. -func MetaTokenSource(newT func() (*Token, error), newTS func(*Token) TokenSource) *metaTokenSource { - return &metaTokenSource{ - newT: newT, - newTS: newTS, - } -} - -// Token returns an oauth2 token or an error -func (ts *metaTokenSource) Token() (*oauth2.Token, error) { - token, err := ts.TokenEx() - if err != nil { - return nil, err - } - - return &token.Token, err -} - -// Token returns a vag token or an error -func (ts *metaTokenSource) TokenEx() (*Token, error) { - ts.mu.Lock() - defer ts.mu.Unlock() - - // use token source - if ts.ts != nil { - token, err := ts.ts.TokenEx() - if err == nil { - return token, nil - } - } - - // create new start token - token, err := ts.newT() - if err != nil { - return nil, err - } - - // create token source - ts.ts = ts.newTS(token) - - // use token source - token, err = ts.ts.TokenEx() - if err != nil { - // token source doesn't work anymore, reset it - ts.ts = nil - } - - return token, err -} diff --git a/vehicle/vag/vwidentity/oauth2.go b/vehicle/vag/vwidentity/oauth2.go deleted file mode 100644 index 351cf17e6..000000000 --- a/vehicle/vag/vwidentity/oauth2.go +++ /dev/null @@ -1,89 +0,0 @@ -package vwidentity - -import ( - "context" - "fmt" - "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" - "golang.org/x/oauth2" -) - -// Login performs VW identity login with optional code challenge -func Oauth2Login(log *util.Logger, oc *oauth2.Config, user, password string) (vag.TokenSource, error) { - oc.Endpoint = Config.NewProvider(context.Background()).Endpoint() - - // add code challenge - cv := oauth2.GenerateVerifier() - q := url.Values{ - "response_type": {"code id_token"}, - "client_id": {oc.ClientID}, - "redirect_uri": {oc.RedirectURL}, - "code_challenge_method": {"S256"}, - "code_challenge": {oauth2.S256ChallengeFromVerifier(cv)}, - "scope": {strings.Join(oc.Scopes, " ")}, - } - - uri := fmt.Sprintf("%s?%s", oc.Endpoint.AuthURL, q.Encode()) - - vwi := New(log) - q, err := vwi.Login(uri, user, password) - if err != nil { - return nil, err - } - - if err := urlvalues.Require(q, "id_token", "code"); err != nil { - return nil, err - } - - os := &Oauth2Service{Helper: request.NewHelper(log), Config: oc} - - token, err := os.retrieveToken(url.Values{ - "client_id": {oc.ClientID}, - "client_secret": {oc.ClientSecret}, - "redirect_uri": {oc.RedirectURL}, - "grant_type": {"authorization_code"}, - "code": {q.Get("code")}, - "code_verifier": {cv}, - }) - if err != nil { - return nil, err - } - - return os.TokenSource(token), nil -} - -type Oauth2Service struct { - *oauth2.Config - *request.Helper -} - -func (v *Oauth2Service) retrieveToken(data url.Values) (*vag.Token, error) { - var res vag.Token - - req, err := request.New(http.MethodPost, v.Endpoint.TokenURL, strings.NewReader(data.Encode()), request.URLEncoding, request.AcceptJSON) - if err == nil { - err = v.DoJSON(req, &res) - } - - return &res, err -} - -func (v *Oauth2Service) refresh(token *vag.Token) (*vag.Token, error) { - return v.retrieveToken(url.Values{ - "client_id": {v.ClientID}, - "client_secret": {v.ClientSecret}, - "grant_type": {"refresh_token"}, - "refresh_token": {token.RefreshToken}, - }) -} - -// TokenSource creates token source. Token is refreshed automatically. -func (v *Oauth2Service) TokenSource(token *vag.Token) vag.TokenSource { - return vag.RefreshTokenSource(token, v.refresh) -} diff --git a/vehicle/vw/eudataact/types.go b/vehicle/vw/eudataact/types.go index 6b28505bf..3c87158ab 100644 --- a/vehicle/vw/eudataact/types.go +++ b/vehicle/vw/eudataact/types.go @@ -28,11 +28,6 @@ var brands = map[string]brand{ "Cupra": {"f85e5b69-e3b2-43aa-9c0d-1b7d0e0b576f@apps_vw-dilab_com", "CUPRA"}, } -// Brands returns the supported brand names -func Brands() []string { - return []string{"Volkswagen", "Audi", "Skoda", "Seat", "Cupra"} -} - // resolveBrand looks up a brand by name, case-insensitively func resolveBrand(name string) (brand, bool) { for k, b := range brands {