Add Tesla using official vehicle command library (#10802)

This commit is contained in:
andig 2024-01-24 12:09:40 +01:00 • committed by GitHub
parent 19ea3fb60c
commit fd33a0d925
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 570 additions and 3 deletions

View file

@ -9,7 +9,8 @@ endif
VERSION := $(if $(TAG_NAME),$(TAG_NAME),$(SHA))
BUILD_DATE := $(shell date -u '+%Y-%m-%d_%H:%M:%S')
BUILD_TAGS := -tags=release
LD_FLAGS := -X github.com/evcc-io/evcc/server.Version=$(VERSION) -X github.com/evcc-io/evcc/server.Commit=$(COMMIT) -s -w
TESLA_CLIENT_ID := ${TESLA_CLIENT_ID}
LD_FLAGS := -X github.com/evcc-io/evcc/server.Version=$(VERSION) -X github.com/evcc-io/evcc/server.Commit=$(COMMIT) -X github.com/evcc-io/evcc/vehicle/tesla-vehicle-command.OAuth2Config.ClientID=$(TESLA_CLIENT_ID) -s -w
BUILD_ARGS := -trimpath -ldflags='$(LD_FLAGS)'
# docker

1
go.mod
View file

@ -85,6 +85,7 @@ require (
github.com/spf13/pflag v1.0.5
github.com/spf13/viper v1.18.2
github.com/stretchr/testify v1.8.4
github.com/teslamotors/vehicle-command v0.0.2
github.com/traefik/yaegi v0.15.1
github.com/tv42/httpunix v0.0.0-20191220191345-2ba4b9c3382c
github.com/volkszaehler/mbmd v0.0.0-20231215091549-af16b1f597b9

10
go.sum
View file

@ -7,6 +7,8 @@ github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1r
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8=
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/JuulLabs-OSS/cbgo v0.0.1 h1:A5JdglvFot1J9qYR0POZ4qInttpsVPN9lqatjaPp2ro=
github.com/JuulLabs-OSS/cbgo v0.0.1/go.mod h1:L4YtGP+gnyD84w7+jN66ncspFRfOYB5aj9QSXaFHmBA=
github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
@ -164,6 +166,8 @@ github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec
github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc=
github.com/glebarez/sqlite v1.10.0 h1:u4gt8y7OND/cCei/NMHmfbLxF6xP2wgKcT/BJf2pYkc=
github.com/glebarez/sqlite v1.10.0/go.mod h1:IJ+lfSOmiekhQsFTJRx/lHtGYmCdtAiTaf5wI9u5uHA=
github.com/go-ble/ble v0.0.0-20220207185428-60d1eecf2633 h1:ZrzoZQz1CF33SPHLkjRpnVuZwr9cO1lTEc4Js7SgBos=
github.com/go-ble/ble v0.0.0-20220207185428-60d1eecf2633/go.mod h1:fFJl/jD/uyILGBeD5iQ8tYHrPlJafyqCJzAyTHNJ1Uk=
github.com/go-http-utils/etag v0.0.0-20161124023236-513ea8f21eb1 h1:zga7zaRE8HCbWjcXMDlfvmQtH0/kMVLo7cQ48dy6kWg=
github.com/go-http-utils/etag v0.0.0-20161124023236-513ea8f21eb1/go.mod h1:PumS+5d59wmAGsZo6IfRpVNaJUq+6xjC4Utt/k8GO6Q=
github.com/go-http-utils/fresh v0.0.0-20161124030543-7231e26a4b27 h1:O6yi4xa9b2DMosGsXzlMe2E9qXgXCVkRLCoRX+5amxI=
@ -439,6 +443,8 @@ github.com/mergermarket/go-pkcs7 v0.0.0-20170926155232-153b18ea13c9/go.mod h1:GH
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/mgutz/logxi v0.0.0-20161027140823-aebf8a7d67ab h1:n8cgpHzJ5+EDyDri2s/GC7a9+qK3/YEGnBsd0uS/8PY=
github.com/mgutz/logxi v0.0.0-20161027140823-aebf8a7d67ab/go.mod h1:y1pL58r5z2VvAjeG1VLGc8zOQgSOzbKN7kMHPvFXJ+8=
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/miekg/dns v1.1.43/go.mod h1:+evo5L0630/F6ca/Z9+GAqzhjGyn8/c+TBaOyfEl0V4=
github.com/miekg/dns v1.1.57 h1:Jzi7ApEIzwEPLHWRcafCN9LZSBbqQpxjt/wpgvg7wcM=
@ -576,6 +582,8 @@ github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4O
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
github.com/raff/goble v0.0.0-20190909174656-72afc67d6a99 h1:JtoVdxWJ3tgyqtnPq3r4hJ9aULcIDDnPXBWxZsdmqWU=
github.com/raff/goble v0.0.0-20190909174656-72afc67d6a99/go.mod h1:CxaUhijgLFX0AROtH5mluSY71VqpjQBw9JXE2UKZmc4=
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/relvacode/iso8601 v1.3.0 h1:HguUjsGpIMh/zsTczGN3DVJFxTU/GX+MMmzcKoMO7ko=
github.com/relvacode/iso8601 v1.3.0/go.mod h1:FlNp+jz+TXpyRqgmM7tnzHHzBnz776kmAH2h3sZCn0I=
@ -671,6 +679,8 @@ github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSW
github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA=
github.com/teivah/onecontext v1.3.0 h1:tbikMhAlo6VhAuEGCvhc8HlTnpX4xTNPTOseWuhO1J0=
github.com/teivah/onecontext v1.3.0/go.mod h1:hoW1nmdPVK/0jrvGtcx8sCKYs2PiS4z0zzfdeuEVyb0=
github.com/teslamotors/vehicle-command v0.0.2 h1:NOoI7d5OVq+Yeaom7iv31sbUMPDhJ/2QRKT4HKHfTa8=
github.com/teslamotors/vehicle-command v0.0.2/go.mod h1:SydThpjTNvhUXBy1VyiD/A9scQSYcp+E6iElHa36Gk0=
github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/traefik/yaegi v0.15.1 h1:YA5SbaL6HZA0Exh9T/oArRHqGN2HQ+zgmCY7dkoTXu4=
github.com/traefik/yaegi v0.15.1/go.mod h1:AVRxhaI2G+nUsaM1zyktzwXn69G3t/AuTDrCiTds9p0=

View file

@ -0,0 +1,56 @@
template: tesla-command
products:
- brand: Tesla
description:
generic: Vehicle-Command API
requirements:
description:
de: |
Es wird ein `access` und ein `refresh` Token für die Kommunikation mit der Tesla API erstellt werden.
Die Tokens werden unter https://tesla.evcc.io erstellt.
en: |
You need to generate an `access` and a `refresh` token for communicating with the Tesla API.
Tokens are generated using https://tesla.evcc.io.
params:
- name: title
- name: icon
default: car
advanced: true
- name: accessToken
required: true
mask: true
help:
en: "See https://docs.evcc.io/en/docs/devices/vehicles#tesla-command"
de: "Siehe https://docs.evcc.io/docs/devices/vehicles#tesla-command"
- name: refreshToken
required: true
mask: true
help:
en: "See https://docs.evcc.io/en/docs/devices/vehicles#tesla-command"
de: "Siehe https://docs.evcc.io/docs/devices/vehicles#tesla-command"
- name: vin
example: W...
- name: capacity
- name: phases
advanced: true
- preset: vehicle-identify
render: |
type: tesla-command
{{- if .title }}
title: {{ .title }}
{{- end }}
{{- if .icon }}
icon: {{ .icon }}
{{- end }}
tokens:
access: {{ .accessToken }}
refresh: {{ .refreshToken }}
capacity: {{ .capacity }}
{{- if .phases }}
phases: {{ .phases }}
{{- end }}
{{- if .vin }}
vin: {{ .vin }}
{{- end }}
{{ include "vehicle-identify" . }}
features: ["coarsecurrent"]

119
vehicle/tesla-command.go Normal file
View file

@ -0,0 +1,119 @@
package vehicle
import (
"context"
"os"
"time"
"github.com/evcc-io/evcc/api"
"github.com/evcc-io/evcc/util"
"github.com/evcc-io/evcc/util/request"
vc "github.com/evcc-io/evcc/vehicle/tesla-vehicle-command"
"golang.org/x/oauth2"
)
// TeslaCommand is an api.Vehicle implementation for Tesla cars using the official Tesla vehicle-command api.
type TeslaCommand struct {
*embed
*vc.Provider
}
func init() {
if id := os.Getenv("TESLA_CLIENT_ID"); id != "" {
vc.OAuth2Config.ClientID = id
}
if secret := os.Getenv("TESLA_CLIENT_SECRET"); secret != "" {
vc.OAuth2Config.ClientSecret = secret
}
if vc.OAuth2Config.ClientID == "" {
registry.Add("tesla-command", NewTeslaCommandFromConfig)
}
}
// const privateKeyFile = "tesla-privatekey.pem"
// NewTeslaCommandFromConfig creates a new vehicle
func NewTeslaCommandFromConfig(other map[string]interface{}) (api.Vehicle, error) {
cc := struct {
embed `mapstructure:",squash"`
Tokens Tokens
VIN string
Timeout time.Duration
Cache time.Duration
}{
Timeout: 10 * time.Second,
Cache: interval,
}
if err := util.DecodeOther(other, &cc); err != nil {
return nil, err
}
if err := cc.Tokens.Error(); err != nil {
return nil, err
}
log := util.NewLogger("tesla-command").Redact(vc.OAuth2Config.ClientID, vc.OAuth2Config.ClientSecret)
client := request.NewClient(log)
ctx := context.WithValue(context.Background(), oauth2.HTTPClient, client)
ts := vc.OAuth2Config.TokenSource(ctx, &oauth2.Token{
AccessToken: cc.Tokens.Access,
RefreshToken: cc.Tokens.Refresh,
Expiry: time.Now(),
})
identity, err := vc.NewIdentity(log, ts)
if err != nil {
return nil, err
}
api := vc.NewAPI(log, identity, cc.Timeout)
vehicle, err := ensureVehicleEx(
cc.VIN, api.Vehicles,
func(v *vc.Vehicle) string {
return v.Vin
},
)
if err != nil {
return nil, err
}
v := &TeslaCommand{
embed: &cc.embed,
Provider: vc.NewProvider(api, vehicle.ID, cc.Cache),
}
if v.Title_ == "" {
v.Title_ = vehicle.DisplayName
}
/*
privKey, err := protocol.LoadPrivateKey(privateKeyFile)
if err != nil {
log.WARN.Println("private key not found, commands are disabled")
return v, nil
}
vv, err := identity.Account().GetVehicle(context.Background(), vehicle.Vin, privKey, cache.New(8))
if err != nil {
return nil, err
}
cs, err := vc.NewCommandSession(vv, cc.Timeout)
if err != nil {
return nil, err
}
res := &struct {
*TeslaCommand
*vc.CommandSession
}{
TeslaCommand: v,
CommandSession: cs,
}
*/
return v, nil
}

View file

@ -0,0 +1,74 @@
package vc
import (
"fmt"
"time"
"github.com/bogosj/tesla"
"github.com/evcc-io/evcc/util"
"github.com/evcc-io/evcc/util/request"
"golang.org/x/oauth2"
)
const (
FleetAudienceEU = "https://fleet-api.prd.eu.vn.cloud.tesla.com"
)
type API struct {
*request.Helper
identity *Identity
}
func NewAPI(log *util.Logger, identity *Identity, timeout time.Duration) *API {
client := request.NewHelper(log)
client.Client.Timeout = timeout
client.Transport = &oauth2.Transport{
Source: identity,
Base: client.Transport,
}
return &API{
Helper: client,
identity: identity,
}
}
func (v *API) Vehicles() ([]*Vehicle, error) {
// ctx, cancel := context.WithTimeout(context.Background(), v.Timeout)
// defer cancel()
// b, err := v.identity.Account().Get(ctx, "api/1/vehicles")
// if err != nil {
// return nil, err
// }
// var res tesla.VehiclesResponse
// if err := json.Unmarshal(b, &res); err != nil {
// return nil, err
// }
var res tesla.VehiclesResponse
err := v.GetJSON(fmt.Sprintf("%s/api/1/vehicles", FleetAudienceEU), &res)
return res.Response, err
}
func (v *API) VehicleData(id int64) (*VehicleData, error) {
// ctx, cancel := context.WithTimeout(context.Background(), request.Timeout)
// defer cancel()
// b, err := v.identity.Account().Get(ctx, fmt.Sprintf("api/1/vehicles/%d/vehicle_data", id))
// if err != nil {
// return nil, err
// }
// var res tesla.VehicleData
// if err := json.Unmarshal(b, &res); err != nil {
// return nil, err
// }
var res tesla.VehicleData
err := v.GetJSON(fmt.Sprintf("%s/api/1/vehicles/%d/vehicle_data", FleetAudienceEU, id), &res)
return &res, err
}

View file

@ -0,0 +1,18 @@
package vc
import (
"errors"
"strings"
"github.com/evcc-io/evcc/api"
"github.com/teslamotors/vehicle-command/pkg/connector/inet"
)
// ApiError converts HTTP 408 error to ErrTimeout
func ApiError(err error) error {
if err != nil && (errors.Is(err, inet.ErrVehicleNotAwake) ||
strings.HasSuffix(err.Error(), "408 Request Timeout") || strings.HasSuffix(err.Error(), "408 (Request Timeout)")) {
err = api.ErrAsleep
}
return err
}

View file

@ -0,0 +1,67 @@
package vc
import (
"github.com/evcc-io/evcc/util"
"github.com/teslamotors/vehicle-command/pkg/account"
"golang.org/x/oauth2"
)
// https://auth.tesla.com/oauth2/v3/.well-known/openid-configuration
// OAuth2Config is the OAuth2 configuration for authenticating with the Tesla API.
var OAuth2Config = &oauth2.Config{
RedirectURL: "https://auth.tesla.com/void/callback",
Endpoint: oauth2.Endpoint{
AuthURL: "https://auth.tesla.com/en_us/oauth2/v3/authorize",
TokenURL: "https://auth.tesla.com/oauth2/v3/token",
AuthStyle: oauth2.AuthStyleInParams,
},
Scopes: []string{"openid", "email", "offline_access"},
}
const userAgent = "evcc/evcc-io"
type Identity struct {
oauth2.TokenSource
log *util.Logger
token *oauth2.Token
acct *account.Account
}
func NewIdentity(log *util.Logger, ts oauth2.TokenSource) (*Identity, error) {
token, err := ts.Token()
if err != nil {
return nil, err
}
acct, err := account.New(token.AccessToken, userAgent)
if err != nil {
return nil, err
}
return &Identity{
TokenSource: ts,
token: token,
acct: acct,
}, nil
}
func (v *Identity) Account() *account.Account {
token, err := v.Token()
if err != nil {
v.log.ERROR.Println(err)
return v.acct
}
if token.AccessToken != v.token.AccessToken {
acct, err := account.New(token.AccessToken, userAgent)
if err != nil {
v.log.ERROR.Println(err)
return v.acct
}
v.acct = acct
}
return v.acct
}

View file

@ -0,0 +1,125 @@
package vc
import (
"time"
"github.com/evcc-io/evcc/api"
"github.com/evcc-io/evcc/provider"
)
type Provider struct {
dataG func() (*VehicleData, error)
}
func NewProvider(api *API, vid int64, cache time.Duration) *Provider {
impl := &Provider{
dataG: provider.Cached(func() (*VehicleData, error) {
return api.VehicleData(vid)
}, cache),
}
return impl
}
// Soc implements the api.Vehicle interface
func (v *Provider) Soc() (float64, error) {
res, err := v.dataG()
if err != nil {
return 0, err
}
return float64(res.Response.ChargeState.UsableBatteryLevel), nil
}
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.dataG()
if err != nil {
return status, err
}
switch res.Response.ChargeState.ChargingState {
case "Stopped", "NoPower", "Complete":
status = api.StatusB
case "Charging":
status = api.StatusC
}
return status, nil
}
var _ api.ChargeRater = (*Provider)(nil)
// ChargedEnergy implements the api.ChargeRater interface
func (v *Provider) ChargedEnergy() (float64, error) {
res, err := v.dataG()
if err != nil {
return 0, err
}
return res.Response.ChargeState.ChargeEnergyAdded, nil
}
var _ api.VehicleRange = (*Provider)(nil)
// Range implements the api.VehicleRange interface
func (v *Provider) Range() (int64, error) {
res, err := v.dataG()
if err != nil {
return 0, err
}
// miles to km
return int64(kmPerMile * res.Response.ChargeState.BatteryRange), nil
}
var _ api.VehicleOdometer = (*Provider)(nil)
const kmPerMile = 1.609344
// Odometer implements the api.VehicleOdometer interface
func (v *Provider) Odometer() (float64, error) {
res, err := v.dataG()
if err != nil {
return 0, err
}
// miles to km
return kmPerMile * res.Response.VehicleState.Odometer, nil
}
var _ api.VehicleFinishTimer = (*Provider)(nil)
// FinishTime implements the api.VehicleFinishTimer interface
func (v *Provider) FinishTime() (time.Time, error) {
res, err := v.dataG()
if err != nil {
return time.Time{}, err
}
return time.Now().Add(time.Duration(res.Response.ChargeState.MinutesToFullCharge) * time.Minute), nil
}
// TODO api.Climater implementation has been removed as it drains battery. Re-check at a later time.
// var _ api.VehiclePosition = (*Provider)(nil)
// // Position implements the api.VehiclePosition interface
// func (v *Provider) Position() (float64, float64, error) {
// res, err := v.dataG()
// if err != nil {
// return 0, 0, err
// }
// if res.Response.DriveState.Latitude != 0 || res.Response.DriveState.Longitude != 0 {
// return res.Response.DriveState.Latitude, res.Response.DriveState.Longitude, nil
// }
// return res.Response.DriveState.ActiveRouteLatitude, res.Response.DriveState.ActiveRouteLongitude, nil
// }
var _ api.SocLimiter = (*Provider)(nil)
// TargetSoc implements the api.SocLimiter interface
func (v *Provider) TargetSoc() (float64, error) {
res, err := v.dataG()
if err != nil {
return 0, err
}
return float64(res.Response.ChargeState.ChargeLimitSoc), nil
}

View file

@ -0,0 +1,86 @@
package vc
import (
"context"
"errors"
"slices"
"time"
"github.com/evcc-io/evcc/api"
"github.com/teslamotors/vehicle-command/pkg/protocol/protobuf/universalmessage"
"github.com/teslamotors/vehicle-command/pkg/vehicle"
)
type CommandSession struct {
vehicle *vehicle.Vehicle
timeout time.Duration
}
func NewCommandSession(vv *vehicle.Vehicle, timeout time.Duration) (*CommandSession, error) {
if err := vv.Connect(context.Background()); err != nil {
return nil, err
}
if err := vv.StartSession(context.Background(), []universalmessage.Domain{universalmessage.Domain_DOMAIN_INFOTAINMENT}); err != nil {
return nil, err
}
v := &CommandSession{
vehicle: vv,
timeout: timeout,
}
return v, nil
}
var _ api.CurrentController = (*CommandSession)(nil)
// MaxCurrent implements the api.CurrentController interface
func (v *CommandSession) MaxCurrent(current int64) error {
ctx, cancel := context.WithTimeout(context.Background(), v.timeout)
defer cancel()
return ApiError(v.vehicle.SetChargingAmps(ctx, int32(current)))
}
var _ api.Resurrector = (*CommandSession)(nil)
// WakeUp implements the api.Resurrector interface
func (v *CommandSession) WakeUp() error {
ctx, cancel := context.WithTimeout(context.Background(), v.timeout)
defer cancel()
return ApiError(v.vehicle.Wakeup(ctx))
}
var _ api.VehicleChargeController = (*CommandSession)(nil)
// StartCharge implements the api.VehicleChargeController interface
func (v *CommandSession) StartCharge() error {
ctx, cancel := context.WithTimeout(context.Background(), v.timeout)
defer cancel()
err := ApiError(v.vehicle.ChargeStart(ctx))
// ignore charging or complete
if err != nil && slices.Contains([]string{"complete", "is_charging"}, err.Error()) {
err = nil
}
return err
}
// StopCharge implements the api.VehicleChargeController interface
func (v *CommandSession) StopCharge() error {
ctx, cancel := context.WithTimeout(context.Background(), v.timeout)
defer cancel()
err := ApiError(v.vehicle.ChargeStop(ctx))
// ignore sleeping vehicle
if errors.Is(err, api.ErrAsleep) {
err = nil
}
return err
}

View file

@ -0,0 +1,9 @@
package vc
import "github.com/bogosj/tesla"
type (
Vehicle = tesla.Vehicle
VehicleData = tesla.VehicleData
CommandResponse = tesla.CommandResponse
)

View file

@ -197,13 +197,14 @@ func (v *Tesla) TargetSoc() (float64, error) {
var _ api.CurrentController = (*Tesla)(nil)
// StartCharge implements the api.VehicleChargeController interface
// MaxCurrent implements the api.CurrentLimiter interface
func (v *Tesla) MaxCurrent(current int64) error {
return v.apiError(v.vehicle.SetChargingAmps(int(current)))
}
var _ api.Resurrector = (*Tesla)(nil)
// WakeUp implements the api.Resurrector interface
func (v *Tesla) WakeUp() error {
_, err := v.vehicle.Wakeup()
return v.apiError(err)

View file

@ -27,7 +27,7 @@ type Tokens struct {
// Error validates the token and returns an error if they are incomplete
func (t *Tokens) Error() error {
if t.Access == "" || t.Refresh == "" {
if t.Access == "" && t.Refresh == "" {
return errors.New("missing access and/or refresh token, use `evcc token` to create")
}