diff --git a/Makefile b/Makefile index 7726690be..f335452fe 100644 --- a/Makefile +++ b/Makefile @@ -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 diff --git a/go.mod b/go.mod index 522556f1f..a57bfa95e 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index d8c8b6d8e..01149f507 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/templates/definition/vehicle/tesla-command.yaml b/templates/definition/vehicle/tesla-command.yaml new file mode 100644 index 000000000..c6708da85 --- /dev/null +++ b/templates/definition/vehicle/tesla-command.yaml @@ -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"] diff --git a/vehicle/tesla-command.go b/vehicle/tesla-command.go new file mode 100644 index 000000000..d85a3bd8d --- /dev/null +++ b/vehicle/tesla-command.go @@ -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 +} diff --git a/vehicle/tesla-vehicle-command/api.go b/vehicle/tesla-vehicle-command/api.go new file mode 100644 index 000000000..6297509cd --- /dev/null +++ b/vehicle/tesla-vehicle-command/api.go @@ -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 +} diff --git a/vehicle/tesla-vehicle-command/helper.go b/vehicle/tesla-vehicle-command/helper.go new file mode 100644 index 000000000..96902e5fe --- /dev/null +++ b/vehicle/tesla-vehicle-command/helper.go @@ -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 +} diff --git a/vehicle/tesla-vehicle-command/identity.go b/vehicle/tesla-vehicle-command/identity.go new file mode 100644 index 000000000..18c90212a --- /dev/null +++ b/vehicle/tesla-vehicle-command/identity.go @@ -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 +} diff --git a/vehicle/tesla-vehicle-command/provider.go b/vehicle/tesla-vehicle-command/provider.go new file mode 100644 index 000000000..565a4b236 --- /dev/null +++ b/vehicle/tesla-vehicle-command/provider.go @@ -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 +} diff --git a/vehicle/tesla-vehicle-command/session.go b/vehicle/tesla-vehicle-command/session.go new file mode 100644 index 000000000..5e9c650b1 --- /dev/null +++ b/vehicle/tesla-vehicle-command/session.go @@ -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 +} diff --git a/vehicle/tesla-vehicle-command/types.go b/vehicle/tesla-vehicle-command/types.go new file mode 100644 index 000000000..2a5b52cb4 --- /dev/null +++ b/vehicle/tesla-vehicle-command/types.go @@ -0,0 +1,9 @@ +package vc + +import "github.com/bogosj/tesla" + +type ( + Vehicle = tesla.Vehicle + VehicleData = tesla.VehicleData + CommandResponse = tesla.CommandResponse +) diff --git a/vehicle/tesla.go b/vehicle/tesla.go index cf739f846..be7b3bacd 100644 --- a/vehicle/tesla.go +++ b/vehicle/tesla.go @@ -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) diff --git a/vehicle/types.go b/vehicle/types.go index 21a128b25..ebbda1199 100644 --- a/vehicle/types.go +++ b/vehicle/types.go @@ -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") }