Volvo: fix connected api (#22078)
This commit is contained in:
parent
8f3280593a
commit
b872dbf23d
6 changed files with 108 additions and 60 deletions
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue