From b872dbf23dbfdf07132ac3d595a7491cb2f2776e Mon Sep 17 00:00:00 2001 From: lehmanju Date: Fri, 27 Jun 2025 06:23:29 +0000 Subject: [PATCH] Volvo: fix connected api (#22078) --- plugin/auth/oauth.go | 26 +++++- .../definition/vehicle/volvo-connected.yaml | 16 ++-- vehicle/volvo/connected/api.go | 14 +--- vehicle/volvo/connected/oauth2.go | 2 +- vehicle/volvo/connected/provider.go | 27 +++--- vehicle/volvo/connected/types.go | 83 +++++++++++++------ 6 files changed, 108 insertions(+), 60 deletions(-) diff --git a/plugin/auth/oauth.go b/plugin/auth/oauth.go index 8861277ad..db81ff3bb 100644 --- a/plugin/auth/oauth.go +++ b/plugin/auth/oauth.go @@ -5,6 +5,9 @@ package auth import ( "context" + "crypto/sha256" + "encoding/hex" + "encoding/json" "net/http" "strings" "sync" @@ -57,16 +60,26 @@ func NewOauthFromConfig(ctx context.Context, other map[string]any) (Authorizer, } func NewOauth(ctx context.Context, cc oauth2.Config) (*OAuth, error) { - // TODO subject should include hash of complete oauth2 config - subject := "oauth." + cc.ClientID + log := util.NewLogger("oauth-generic") + + // generate json string from oauth2 config + bytejson, err := json.Marshal(cc) + + if err != nil { + log.ERROR.Printf("error converting oauth config to json: %s", err) + } + + h := sha256.New() + h.Write(bytejson) + sha1_hash := hex.EncodeToString(h.Sum(nil)) + + subject := sha1_hash // reuse instance if instance := getInstance(subject); instance != nil { return instance, nil } - log := util.NewLogger("oauth-generic") - // create new instance o := &OAuth{ subject: subject, @@ -78,6 +91,8 @@ func NewOauth(ctx context.Context, cc oauth2.Config) (*OAuth, error) { // load token from db var tok oauth2.Token if settings.Exists(o.subject) { + o.log.DEBUG.Printf("loading token for %s from database", o.subject) + if err := settings.Json(o.subject, &tok); err != nil { return nil, err } @@ -111,6 +126,9 @@ func (o *OAuth) RefreshToken(token *oauth2.Token) (*oauth2.Token, error) { return nil, api.ErrMissingToken } + // log token before refresh + o.log.DEBUG.Printf("refreshing token for %s", o.subject) + // refresh token source token, err := o.cc.TokenSource(o.ctx, token).Token() if err != nil { diff --git a/templates/definition/vehicle/volvo-connected.yaml b/templates/definition/vehicle/volvo-connected.yaml index b171eabac..9c68354e2 100644 --- a/templates/definition/vehicle/volvo-connected.yaml +++ b/templates/definition/vehicle/volvo-connected.yaml @@ -6,20 +6,20 @@ requirements: de: | Für die Nutzung mit EVCC benötigst du einen Volvo Account und einen Volvo Connected Car API Key. Erstelle dazu auf der [Account Seite](https://developer.volvocars.com/account/) eine neue Applikation und speichere den primären VCC API Key ab. - Veröffentliche nun deine Applikation und wähle unter "Scopes" die Berechtigungen "Connected Vehicle API -> conve:vehicle-relation" und "Energy API -> (alles)" aus. - Als Redirect URL musst du die URL deiner EVCC Instanz eintragen, zb "http://evcc.local:7070/oauth/callback". + Veröffentliche nun deine Applikation und wähle unter "Scopes" die Berechtigungen "Connected Vehicle API -> conve:vehicle-relation" und "Energy API -> energy:state:read" aus. + Als Redirect URL musst du die URL deiner EVCC Instanz eintragen, zb "https://evcc.example.org/oauth/callback". Sobald die Applikation erstellt ist, wird sie als "Publication under Review" angezeigt. Das ist nicht weiter schlimm, es funktioniert trotzdem. Beim Anlegen des Fahrzeugs über die UI wird ein Fehler angezeigt. - Schaue im Log nach der Meldung "registering oauth provider at /oauth/login?..." und öffne den Link "http://evcc.local:7070/oauth/login?..." in einem neuen Tab. + Schaue im Log nach der Meldung "registering oauth provider at /oauth/login?..." und öffne den Link "https://evcc.example.org/oauth/login?..." in einem neuen Tab. Melde dich mit deinem Volvo Account an und erlaube den Zugriff auf die Daten. Ist die Autorisierung erfolgreich, kann das Fahrzeug hinzugefügt werden. en: | To use with EVCC, you need a Volvo account and a Volvo Connected Car API Key. To do this, create a new application on the [Account page](https://developer.volvocars.com/account/) and save the primary VCC API key. - Now publish your application and select the permissions "Connected Vehicle API -> conve:vehicle-relation" and "Energy API -> (everything)" under "Scopes". - You must enter the URL of your EVCC instance as the redirect URL, e.g. "http://evcc.local:7070/oauth/callback". + Now publish your application and select the permissions "Connected Vehicle API -> conve:vehicle-relation" and "Energy API -> energy:state:read" under "Scopes". + You must enter the URL of your EVCC instance as the redirect URL, e.g. "https://evcc.example.org/oauth/callback". Once the application is created, it will be displayed as "Publication under Review". This is not a problem, it still works. When adding the vehicle via the UI, an error message is displayed. - Check the log for the message "registering oauth provider at /oauth/login?..." and open the link "http://evcc.local:7070/oauth/login?..." in a new tab. + Check the log for the message "registering oauth provider at /oauth/login?..." and open the link "https://evcc.example.org/oauth/login?..." in a new tab. Log in with your Volvo account and allow access to the data. If the authorization is successful, the vehicle can be added. params: - preset: vehicle-common @@ -45,8 +45,8 @@ params: description: generic: Redirect URI help: - en: "Redirect URI of your evcc instance, format: `http://evcc.local:7070/oauth/callback`. Must match the redirect URI set in your Volvo Developer App." - de: "Redirect-URI deiner evcc-Instanz, Format: http://evcc.local:7070/oauth/callback. Muss mit der Redirect-URI übereinstimmen, die in deiner Volvo Developer App festgelegt ist." + en: "Redirect URI of your EVCC instance, format: `https://evcc.example.org/oauth/callback`. Must match the redirect URI set in your Volvo Developer App." + de: "Redirect-URI deiner EVCC-Instanz, Format: https://evcc.example.org/oauth/callback. Muss mit der Redirect-URI übereinstimmen, die in deiner Volvo Developer App festgelegt ist." - name: vin example: WF0FXX... - name: accessToken diff --git a/vehicle/volvo/connected/api.go b/vehicle/volvo/connected/api.go index be89beb12..a8bf4130b 100644 --- a/vehicle/volvo/connected/api.go +++ b/vehicle/volvo/connected/api.go @@ -2,7 +2,6 @@ package connected import ( "fmt" - "net/http" "github.com/evcc-io/evcc/plugin/auth" "github.com/evcc-io/evcc/util" @@ -52,16 +51,11 @@ func (v *API) Vehicles() ([]string, error) { } // Range provides range status api response -func (v *API) RechargeStatus(vin string) (RechargeStatus, error) { - uri := fmt.Sprintf("%s/energy/v1/vehicles/%s/recharge-status", ApiURL, vin) - req, err := request.New(http.MethodGet, uri, nil, map[string]string{ - "Accept": "application/vnd.volvocars.api.energy.vehicledata.v1+json", - }) +func (v *API) EnergyState(vin string) (EnergyState, error) { + uri := fmt.Sprintf("%s/energy/v2/vehicles/%s/state", ApiURL, vin) - var res RechargeStatus - if err == nil { - err = v.DoJSON(req, &res) - } + var res EnergyState + err := v.GetJSON(uri, &res) return res, err } diff --git a/vehicle/volvo/connected/oauth2.go b/vehicle/volvo/connected/oauth2.go index 75710836e..41afef61c 100644 --- a/vehicle/volvo/connected/oauth2.go +++ b/vehicle/volvo/connected/oauth2.go @@ -18,7 +18,7 @@ func Oauth2Config(id, secret, redirecturi string) *oauth2.Config { Scopes: []string{ oidc.ScopeOpenID, "conve:vehicle_relation", - "energy:recharge_status", "energy:battery_charge_level", "energy:electric_range", "energy:estimated_charging_time", "energy:charging_connection_status", "energy:charging_system_status", + "energy:state:read", }, } } diff --git a/vehicle/volvo/connected/provider.go b/vehicle/volvo/connected/provider.go index 2b04d8fd0..7106584f2 100644 --- a/vehicle/volvo/connected/provider.go +++ b/vehicle/volvo/connected/provider.go @@ -9,14 +9,14 @@ import ( // Provider implements the vehicle api type Provider struct { - statusG func() (RechargeStatus, error) + statusG func() (EnergyState, error) } // NewProvider creates a vehicle api provider func NewProvider(api *API, vin string, cache time.Duration) *Provider { impl := &Provider{ - statusG: util.Cached(func() (RechargeStatus, error) { - return api.RechargeStatus(vin) + statusG: util.Cached(func() (EnergyState, error) { + return api.EnergyState(vin) }, cache), } return impl @@ -25,7 +25,7 @@ func NewProvider(api *API, vin string, cache time.Duration) *Provider { // Soc implements the api.Vehicle interface func (v *Provider) Soc() (float64, error) { res, err := v.statusG() - return res.Data.BatteryChargeLevel.Value, err + return res.BatteryChargeLevel.Value, err } // Range implements the api.ChargeState interface @@ -37,14 +37,14 @@ func (v *Provider) Status() (api.ChargeStatus, error) { return status, nil } - switch res.Data.ChargingConnectionStatus.Value { - case "CONNECTION_STATUS_DISCONNECTED": + switch res.ChargingConnectionStatus.Value { + case "DISCONNECTED": status = api.StatusA - case "CONNECTION_STATUS_CONNECTED_AC", "CONNECTION_STATUS_CONNECTED_DC": + case "CONNECTED", "FAULT": status = api.StatusB } - if res.Data.ChargingSystemStatus.Value == "CHARGING_SYSTEM_CHARGING" { + if res.ChargingStatus.Status == "CHARGING" { status = api.StatusC } @@ -56,7 +56,7 @@ var _ api.VehicleRange = (*Provider)(nil) // Range implements the api.VehicleRange interface func (v *Provider) Range() (rng int64, err error) { res, err := v.statusG() - return res.Data.ElectricRange.Value, err + return res.ElectricRange.Value, err } var _ api.VehicleFinishTimer = (*Provider)(nil) @@ -64,5 +64,12 @@ var _ api.VehicleFinishTimer = (*Provider)(nil) // FinishTime implements the api.VehicleFinishTimer interface func (v *Provider) FinishTime() (time.Time, error) { res, err := v.statusG() - return res.Data.EstimatedChargingTime.Timestamp.Add(time.Duration(res.Data.EstimatedChargingTime.Value) * time.Minute), err + return res.EstimatedChargingTimeTimeToTargetBatteryChargeLevel.Timestamp.Add(time.Duration(res.EstimatedChargingTimeTimeToTargetBatteryChargeLevel.Value) * time.Minute), err +} + +// GetLimitSoc implements the api.SocLimiter interface +func (v *Provider) GetLimitSoc() (int64, error) { + res, err := v.statusG() + + return int64(res.TargetBatteryChargeLevel.Value), err } diff --git a/vehicle/volvo/connected/types.go b/vehicle/volvo/connected/types.go index 36b38cca0..75ac9d7b9 100644 --- a/vehicle/volvo/connected/types.go +++ b/vehicle/volvo/connected/types.go @@ -2,33 +2,62 @@ package connected import "time" -type RechargeStatus struct { - Status int - OperationID string - Data struct { - BatteryChargeLevel struct { - Value float64 `json:",string"` - Unit string - Timestamp time.Time - } - ElectricRange struct { - Value int64 `json:",string"` - Unit string - Timestamp time.Time - } - EstimatedChargingTime struct { - Value int64 `json:",string"` - Unit string - Timestamp time.Time - } - ChargingConnectionStatus struct { - Value string - Timestamp time.Time - } - ChargingSystemStatus struct { - Value string - Timestamp time.Time - } +type EnergyState struct { + BatteryChargeLevel struct { + Status string + Value float64 + Unit string + Timestamp time.Time + } + ElectricRange struct { + Status string + Value int64 + Unit string + Timestamp time.Time + } + ChargingConnectionStatus struct { + Status string + Value string + Timestamp time.Time + } + ChargingStatus struct { + Status string + Value string + Timestamp time.Time + } + ChargingType struct { + Status string + Value string + Timestamp time.Time + } + ChargerPowerStatus struct { + Status string + Value string + Timestamp time.Time + } + EstimatedChargingTimeTimeToTargetBatteryChargeLevel struct { + Status string + Value int64 + Unit string + Timestamp time.Time + } + ChargingCurrentLimit struct { + Status string + Value int64 + Unit string + Timestamp time.Time + } + TargetBatteryChargeLevel struct { + Status string + Value float64 + Unit string + Timestamp time.Time + } + ChargingPower struct { + Status string + Value int64 + Unit string + Timestamp time.Time } }