Add Tesla using official vehicle command library (#10802)
This commit is contained in:
parent
19ea3fb60c
commit
fd33a0d925
13 changed files with 570 additions and 3 deletions
3
Makefile
3
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
|
||||
|
|
|
|||
1
go.mod
1
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
|
||||
|
|
|
|||
10
go.sum
10
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=
|
||||
|
|
|
|||
56
templates/definition/vehicle/tesla-command.yaml
Normal file
56
templates/definition/vehicle/tesla-command.yaml
Normal 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
119
vehicle/tesla-command.go
Normal 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
|
||||
}
|
||||
74
vehicle/tesla-vehicle-command/api.go
Normal file
74
vehicle/tesla-vehicle-command/api.go
Normal 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
|
||||
}
|
||||
18
vehicle/tesla-vehicle-command/helper.go
Normal file
18
vehicle/tesla-vehicle-command/helper.go
Normal 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
|
||||
}
|
||||
67
vehicle/tesla-vehicle-command/identity.go
Normal file
67
vehicle/tesla-vehicle-command/identity.go
Normal 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
|
||||
}
|
||||
125
vehicle/tesla-vehicle-command/provider.go
Normal file
125
vehicle/tesla-vehicle-command/provider.go
Normal 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
|
||||
}
|
||||
86
vehicle/tesla-vehicle-command/session.go
Normal file
86
vehicle/tesla-vehicle-command/session.go
Normal 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
|
||||
}
|
||||
9
vehicle/tesla-vehicle-command/types.go
Normal file
9
vehicle/tesla-vehicle-command/types.go
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
package vc
|
||||
|
||||
import "github.com/bogosj/tesla"
|
||||
|
||||
type (
|
||||
Vehicle = tesla.Vehicle
|
||||
VehicleData = tesla.VehicleData
|
||||
CommandResponse = tesla.CommandResponse
|
||||
)
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue