Volvo: fix connected api (#22078)

This commit is contained in:
lehmanju 2025-06-27 06:23:29 +00:00 • committed by GitHub
parent 8f3280593a
commit b872dbf23d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 108 additions and 60 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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