Tesla: unify implementations (BC) (#12277)
This commit is contained in:
parent
5448616912
commit
1ae96688f1
20 changed files with 257 additions and 1060 deletions
|
|
@ -52,8 +52,6 @@ func runToken(cmd *cobra.Command, args []string) {
|
|||
var err error
|
||||
|
||||
switch strings.ToLower(vehicleConf.Type) {
|
||||
case "tesla":
|
||||
token, err = teslaToken()
|
||||
case "tronity":
|
||||
token, err = tronityToken(conf, vehicleConf)
|
||||
default:
|
||||
|
|
|
|||
|
|
@ -1,138 +0,0 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/bogosj/tesla"
|
||||
"github.com/evcc-io/evcc/util/request"
|
||||
"github.com/manifoldco/promptui"
|
||||
"github.com/skratchdot/open-golang/open"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
// copied from https://github.com/bogosj/tesla
|
||||
func getUsernameAndPassword() (string, string, error) {
|
||||
user, err := (&promptui.Prompt{
|
||||
Label: "Username",
|
||||
Pointer: promptui.PipeCursor,
|
||||
Validate: func(s string) error {
|
||||
if len(s) == 0 {
|
||||
return errors.New("len(s) == 0")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}).Run()
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
password, err := (&promptui.Prompt{
|
||||
Label: "Password",
|
||||
Mask: '*',
|
||||
Pointer: promptui.PipeCursor,
|
||||
Validate: func(s string) error {
|
||||
if len(s) == 0 {
|
||||
return errors.New("len(s) == 0")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}).Run()
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
return user, password, nil
|
||||
}
|
||||
|
||||
func codePrompt(ctx context.Context, devices []tesla.Device) (tesla.Device, string, error) {
|
||||
var i int
|
||||
if len(devices) > 1 {
|
||||
var err error
|
||||
i, _, err = (&promptui.Select{
|
||||
Label: "Device",
|
||||
Items: devices,
|
||||
Pointer: promptui.PipeCursor,
|
||||
}).Run()
|
||||
if err != nil {
|
||||
return tesla.Device{}, "", fmt.Errorf("select device: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
code, err := (&promptui.Prompt{
|
||||
Label: "Passcode",
|
||||
Pointer: promptui.PipeCursor,
|
||||
Validate: func(s string) error {
|
||||
if len(s) != 6 {
|
||||
return errors.New("len(s) != 6")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}).Run()
|
||||
if err != nil {
|
||||
return tesla.Device{}, "", err
|
||||
}
|
||||
|
||||
return devices[i], strings.TrimSpace(code), nil
|
||||
}
|
||||
|
||||
func captchaPrompt(ctx context.Context, svg io.Reader) (string, error) {
|
||||
tmpFile, err := os.CreateTemp(os.TempDir(), "evcc-*.svg")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("cannot create temp file: %w", err)
|
||||
}
|
||||
|
||||
if _, err := io.Copy(tmpFile, svg); err != nil {
|
||||
return "", fmt.Errorf("cannot write temp file: %w", err)
|
||||
}
|
||||
|
||||
_ = tmpFile.Close()
|
||||
|
||||
if err := open.Run(tmpFile.Name()); err != nil {
|
||||
return "", fmt.Errorf("cannot open captcha for display: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println("Captcha is now being opened in default application for svg files.")
|
||||
|
||||
captcha, err := (&promptui.Prompt{
|
||||
Label: "Captcha",
|
||||
Pointer: promptui.PipeCursor,
|
||||
Validate: func(s string) error {
|
||||
if len(s) < 4 {
|
||||
return errors.New("len(s) < 4")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}).Run()
|
||||
|
||||
return strings.TrimSpace(captcha), err
|
||||
}
|
||||
|
||||
func teslaToken() (*oauth2.Token, error) {
|
||||
username, password, err := getUsernameAndPassword()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ctx := context.WithValue(context.Background(), oauth2.HTTPClient, request.NewClient(log))
|
||||
client, err := tesla.NewClient(
|
||||
ctx,
|
||||
tesla.WithMFAHandler(codePrompt),
|
||||
tesla.WithCaptchaHandler(captchaPrompt),
|
||||
tesla.WithCredentials(username, password),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
token, err := client.Token()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return token, nil
|
||||
}
|
||||
7
go.mod
7
go.mod
|
|
@ -17,7 +17,6 @@ require (
|
|||
github.com/basgys/goxml2json v1.1.0
|
||||
github.com/basvdlei/gotsmart v0.0.3
|
||||
github.com/benbjohnson/clock v1.3.5
|
||||
github.com/bogosj/tesla v1.3.1
|
||||
github.com/cenkalti/backoff/v4 v4.2.1
|
||||
github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21
|
||||
github.com/containrrr/shoutrrr v0.8.0
|
||||
|
|
@ -28,7 +27,7 @@ require (
|
|||
github.com/eclipse/paho.mqtt.golang v1.4.3
|
||||
github.com/enbility/cemd v0.2.2
|
||||
github.com/enbility/eebus-go v0.2.0
|
||||
github.com/evcc-io/tesla-proxy-client v0.0.0-20240210120646-d0be579ff7b7
|
||||
github.com/evcc-io/tesla-proxy-client v0.0.0-20240217133531-c8fc01cb2151
|
||||
github.com/fatih/structs v1.1.0
|
||||
github.com/glebarez/sqlite v1.10.0
|
||||
github.com/go-http-utils/etag v0.0.0-20161124023236-513ea8f21eb1
|
||||
|
|
@ -62,7 +61,6 @@ require (
|
|||
github.com/lorenzodonini/ocpp-go v0.17.1-0.20231031074144-d214d7c42cbc
|
||||
github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40
|
||||
github.com/mabunixda/wattpilot v1.6.3
|
||||
github.com/manifoldco/promptui v0.9.0
|
||||
github.com/mitchellh/go-homedir v1.1.0
|
||||
github.com/mitchellh/mapstructure v1.5.0
|
||||
github.com/mlnoga/rct v0.1.2-0.20230731074838-03eacb926f99
|
||||
|
|
@ -114,7 +112,6 @@ require (
|
|||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/bitly/go-simplejson v0.5.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/chzyer/readline v1.5.1 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/fatih/color v1.16.0 // indirect
|
||||
|
|
@ -209,3 +206,5 @@ replace github.com/imdario/mergo => github.com/imdario/mergo v0.3.16
|
|||
replace github.com/spf13/viper => github.com/spf13/viper v1.18.1
|
||||
|
||||
replace github.com/mabunixda/wattpilot => github.com/mabunixda/wattpilot v1.6.2
|
||||
|
||||
// replace github.com/evcc-io/tesla-proxy-client => ../tesla-proxy-client
|
||||
|
|
|
|||
18
go.sum
18
go.sum
|
|
@ -74,8 +74,6 @@ github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kB
|
|||
github.com/bitly/go-simplejson v0.5.0 h1:6IH+V8/tVMab511d5bn4M7EwGXZf9Hj6i2xSwkNEM+Y=
|
||||
github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA=
|
||||
github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w=
|
||||
github.com/bogosj/tesla v1.3.1 h1:2iTieXh8jqG8FGWs63aQfRWbD7toLUm6fQ7677eevIE=
|
||||
github.com/bogosj/tesla v1.3.1/go.mod h1:JvkjZAswd288BeRYd37fKqyYKLcDj3E5oY/OuLzndaI=
|
||||
github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ=
|
||||
github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
|
||||
github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
|
||||
|
|
@ -84,15 +82,6 @@ github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA
|
|||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM=
|
||||
github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI=
|
||||
github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04=
|
||||
github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=
|
||||
github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21 h1:tuijfIjZyjZaHq9xDUh0tNitwXshJpbLkqMOJv4H3do=
|
||||
|
|
@ -143,8 +132,8 @@ github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.m
|
|||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/evcc-io/modbus v0.0.0-20230705160100-23e1df2c01c3 h1:QBtLqy85cy9KLRWqKxX+42bN1rWti8iU7pL7Vwf2UbE=
|
||||
github.com/evcc-io/modbus v0.0.0-20230705160100-23e1df2c01c3/go.mod h1:qVX2WhsI5xyAoM6I/MV1bXSKBPdLAjp7pCvieO/S0AY=
|
||||
github.com/evcc-io/tesla-proxy-client v0.0.0-20240210120646-d0be579ff7b7 h1:O2/JI5Od2x2/faJadwMFtVtJnJJxeGGzIjPExcqzD9I=
|
||||
github.com/evcc-io/tesla-proxy-client v0.0.0-20240210120646-d0be579ff7b7/go.mod h1:zWtAweBqXJTk3HSrPSecz3Q3a2hAUQ4vOE6paJfn03I=
|
||||
github.com/evcc-io/tesla-proxy-client v0.0.0-20240217133531-c8fc01cb2151 h1:nnRPTOA++dAAzY/bOJbsXquI1UgSXey4k8so2i6o96E=
|
||||
github.com/evcc-io/tesla-proxy-client v0.0.0-20240217133531-c8fc01cb2151/go.mod h1:zWtAweBqXJTk3HSrPSecz3Q3a2hAUQ4vOE6paJfn03I=
|
||||
github.com/fatih/camelcase v1.0.0/go.mod h1:yN2Sb0lFhZJUdVvtELVWefmrXpuZESvPmqwoZc+/fpc=
|
||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
|
||||
|
|
@ -425,8 +414,6 @@ github.com/mabunixda/wattpilot v1.6.2 h1:xZE/elFntUJgaWTK2V7CJVQRcE+WvkT8g6dCk/o
|
|||
github.com/mabunixda/wattpilot v1.6.2/go.mod h1:2tcdLRoqycO6bWYAjZMP9g1giMgw4t2MnbN3VxaCN1Q=
|
||||
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
|
||||
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||
github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA=
|
||||
github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg=
|
||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
|
|
@ -842,7 +829,6 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||
golang.org/x/sys v0.0.0-20210426080607-c94f62235c83/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
|
|
|
|||
|
|
@ -11,11 +11,14 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/andig/go-powerwall"
|
||||
"github.com/bogosj/tesla"
|
||||
"github.com/evcc-io/evcc/api"
|
||||
"github.com/evcc-io/evcc/provider"
|
||||
"github.com/evcc-io/evcc/util"
|
||||
"github.com/evcc-io/evcc/util/request"
|
||||
"github.com/evcc-io/evcc/util/sponsor"
|
||||
"github.com/evcc-io/evcc/vehicle"
|
||||
"github.com/evcc-io/evcc/vehicle/tesla"
|
||||
teslaclient "github.com/evcc-io/tesla-proxy-client"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
|
|
@ -24,7 +27,7 @@ type PowerWall struct {
|
|||
usage string
|
||||
client *powerwall.Client
|
||||
meterG func() (map[string]powerwall.MeterAggregatesData, error)
|
||||
energySite *tesla.EnergySite
|
||||
energySite *teslaclient.EnergySite
|
||||
}
|
||||
|
||||
func init() {
|
||||
|
|
@ -39,7 +42,7 @@ func NewPowerWallFromConfig(other map[string]interface{}) (api.Meter, error) {
|
|||
cc := struct {
|
||||
URI, Usage, User, Password string
|
||||
Cache time.Duration
|
||||
RefreshToken string
|
||||
Tokens vehicle.Tokens
|
||||
SiteId int64
|
||||
battery `mapstructure:",squash"`
|
||||
}{
|
||||
|
|
@ -70,12 +73,15 @@ func NewPowerWallFromConfig(other map[string]interface{}) (api.Meter, error) {
|
|||
cc.Usage = "solar"
|
||||
}
|
||||
|
||||
return NewPowerWall(cc.URI, cc.Usage, cc.User, cc.Password, cc.Cache, cc.RefreshToken, cc.SiteId, cc.battery)
|
||||
return NewPowerWall(cc.URI, cc.Usage, cc.User, cc.Password, cc.Cache, cc.Tokens, cc.SiteId, cc.battery)
|
||||
}
|
||||
|
||||
// NewPowerWall creates a Tesla PowerWall Meter
|
||||
func NewPowerWall(uri, usage, user, password string, cache time.Duration, refreshToken string, siteId int64, battery battery) (api.Meter, error) {
|
||||
log := util.NewLogger("powerwall").Redact(user, password, refreshToken)
|
||||
func NewPowerWall(uri, usage, user, password string, cache time.Duration, tokens vehicle.Tokens, siteId int64, battery battery) (api.Meter, error) {
|
||||
log := util.NewLogger("powerwall").Redact(
|
||||
user, password,
|
||||
tokens.Access, tokens.Refresh,
|
||||
)
|
||||
|
||||
httpClient := &http.Client{
|
||||
Transport: request.NewTripper(log, powerwall.DefaultTransport()),
|
||||
|
|
@ -93,50 +99,6 @@ func NewPowerWall(uri, usage, user, password string, cache time.Duration, refres
|
|||
meterG: provider.Cached(client.GetMetersAggregates, cache),
|
||||
}
|
||||
|
||||
var batteryControl bool
|
||||
if refreshToken != "" || siteId != 0 {
|
||||
if refreshToken == "" {
|
||||
return nil, errors.New("missing refresh token")
|
||||
}
|
||||
batteryControl = true
|
||||
}
|
||||
|
||||
if batteryControl {
|
||||
ctx := context.WithValue(context.Background(), oauth2.HTTPClient, request.NewClient(log))
|
||||
|
||||
options := []tesla.ClientOption{tesla.WithToken(&oauth2.Token{
|
||||
RefreshToken: refreshToken,
|
||||
Expiry: time.Now(),
|
||||
})}
|
||||
|
||||
cloudClient, err := tesla.NewClient(ctx, options...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if siteId == 0 {
|
||||
// auto detect energy site ID, picking first
|
||||
products, err := cloudClient.Products()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, p := range products {
|
||||
if p.EnergySiteId != 0 {
|
||||
siteId = p.EnergySiteId
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.Redact(strconv.FormatInt(siteId, 10))
|
||||
energySite, err := cloudClient.EnergySite(siteId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m.energySite = energySite
|
||||
}
|
||||
|
||||
// decorate api.MeterEnergy
|
||||
var totalEnergy func() (float64, error)
|
||||
if m.usage == "load" || m.usage == "solar" {
|
||||
|
|
@ -161,7 +123,51 @@ func NewPowerWall(uri, usage, user, password string, cache time.Duration, refres
|
|||
|
||||
// decorate api.BatteryController
|
||||
var batModeS func(api.BatteryMode) error
|
||||
if batteryControl {
|
||||
|
||||
token, err := tokens.Token()
|
||||
if err == nil {
|
||||
if !sponsor.IsAuthorized() {
|
||||
return nil, api.ErrSponsorRequired
|
||||
}
|
||||
|
||||
identity, err := tesla.NewIdentity(log, token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
hc := request.NewClient(log)
|
||||
hc.Transport = &oauth2.Transport{
|
||||
Source: identity,
|
||||
Base: hc.Transport,
|
||||
}
|
||||
|
||||
cloudClient, err := teslaclient.NewClient(context.Background(), teslaclient.WithClient(hc))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if siteId == 0 {
|
||||
// auto detect energy site ID, picking first
|
||||
products, err := cloudClient.Products()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, p := range products {
|
||||
if p.EnergySiteId != 0 {
|
||||
siteId = p.EnergySiteId
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.Redact(strconv.FormatInt(siteId, 10))
|
||||
energySite, err := cloudClient.EnergySite(siteId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m.energySite = energySite
|
||||
|
||||
batModeS = battery.LimitController(m.socG, func(limit float64) error {
|
||||
return m.energySite.SetBatteryReserve(uint64(limit))
|
||||
})
|
||||
|
|
|
|||
|
|
@ -7,19 +7,14 @@ capabilities: ["battery-control"]
|
|||
requirements:
|
||||
description:
|
||||
de: |
|
||||
Um die optionale Entladesteuerung der Battery zu nutzen wird ein `refresh` Token für die Kommunikation mit der Tesla API benötigt.
|
||||
|
||||
Folgende Apps ermöglichen das Erstellen des Tokens:
|
||||
- [Auth app for Tesla (iOS)](https://apps.apple.com/us/app/auth-app-for-tesla/id1552058613#?platform=iphone)
|
||||
- [Tesla Tokens (Android)](https://play.google.com/store/apps/details?id=net.leveugle.teslatokens)
|
||||
- [Tesla Auth (macOS, Linux)](https://github.com/adriankumpf/tesla_auth)
|
||||
Benötigt `access` und `refresh` Tokens. Diese können über [tesla.evcc.io](https://tesla.evcc.io) erstellt werden.
|
||||
Die optionale Batteriesteuerung erfolgt über einen evcc Proxy-Server und erfordert ein Sponsor Token.
|
||||
Siehe https://tesla.evcc.io.
|
||||
en: |
|
||||
To use the optional battery control you need to generate a `refresh` token for communicating with the Tesla API.
|
||||
|
||||
The following apps allow to create the token:
|
||||
- [Auth app for Tesla (iOS)](https://apps.apple.com/us/app/auth-app-for-tesla/id1552058613#?platform=iphone)
|
||||
- [Tesla Tokens (Android)](https://play.google.com/store/apps/details?id=net.leveugle.teslatokens)
|
||||
- [Tesla Auth (macOS, Linux)](https://github.com/adriankumpf/tesla_auth)
|
||||
Teska `access` and `refresh` tokens are required. These can be generated through [tesla.evcc.io](https://tesla.evcc.io).
|
||||
Optional battery control is done via an evcc proxy server and requires a sponsor token.
|
||||
Siehe https://tesla.evcc.io.
|
||||
evcc: ["sponsorship"]
|
||||
params:
|
||||
- name: usage
|
||||
choice: ["grid", "pv", "battery"]
|
||||
|
|
@ -30,6 +25,10 @@ params:
|
|||
help:
|
||||
en: Password of the user "customer". By default this is the last 5 characters of password stated on the Tesla Gateway.
|
||||
de: Passwort des Benutzers "Kunde". Default sind die letzten 5 Zeichen des auf dem Tesla Gateway genannten Passworts.
|
||||
- name: accessToken
|
||||
help:
|
||||
en: "See https://docs.evcc.io/en/docs/devices/meters#tesla-powerwall"
|
||||
de: "Siehe https://docs.evcc.io/docs/devices/meters#tesla-powerwall"
|
||||
- name: refreshToken
|
||||
help:
|
||||
en: "See https://docs.evcc.io/en/docs/devices/meters#tesla-powerwall"
|
||||
|
|
@ -56,7 +55,9 @@ render: |
|
|||
usage: {{ .usage }}
|
||||
user: customer
|
||||
password: {{ .password }} # for user 'customer'
|
||||
refreshToken: {{ .refreshToken }}
|
||||
tokens:
|
||||
access: {{ .accessToken }}
|
||||
refresh: {{ .refreshToken }}
|
||||
siteId: {{ .siteId }}
|
||||
minSoc: {{ .minSoc }}
|
||||
maxSoc: {{ .maxSoc }}
|
||||
|
|
|
|||
|
|
@ -1,61 +0,0 @@
|
|||
template: tesla-command
|
||||
products:
|
||||
- brand: Tesla
|
||||
description:
|
||||
de: aktuell
|
||||
en: latest
|
||||
requirements:
|
||||
description:
|
||||
de: |
|
||||
Du benötigst ein `access` und ein `refresh` Token.
|
||||
Diese kannst du über [tesla.evcc.io](https://tesla.evcc.io) erstellt.
|
||||
|
||||
**Hinweis:** Dies ist die von Tesla offiziell unterstützte [FleetAPI](https://developer.tesla.com/docs/fleet-api).
|
||||
en: |
|
||||
You need an `access` and a `refresh` token.
|
||||
Please generate them through [tesla.evcc.io](https://tesla.evcc.io).
|
||||
|
||||
**Note:** This is Tesla's officially supported [FleetAPI](https://developer.tesla.com/docs/fleet-api).
|
||||
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"]
|
||||
|
|
@ -1,46 +0,0 @@
|
|||
template: tesla-proxy
|
||||
products:
|
||||
- brand: Tesla
|
||||
description:
|
||||
de: Fleet API Proxy
|
||||
en: Fleet API Proxy
|
||||
requirements:
|
||||
description:
|
||||
de: |
|
||||
Es muss ein `access` und ein `refresh` Token für die Kommunikation mit der Tesla API erstellt werden.
|
||||
Für die Steuerung des Fahrzeugs muss ferner der Virtual Key auf dem Fahrzeug installiert werden.
|
||||
Siehe https://tesla.evcc.io.
|
||||
en: |
|
||||
You need to generate an `access` and a `refresh` token for communicating with the Tesla API.
|
||||
For vehicle control, the virtual key must be installed on the vehicle.
|
||||
See https://tesla.evcc.io.
|
||||
evcc: ["sponsorship"]
|
||||
params:
|
||||
- name: title
|
||||
- name: icon
|
||||
default: car
|
||||
advanced: true
|
||||
- name: accessToken
|
||||
required: true
|
||||
mask: true
|
||||
- name: refreshToken
|
||||
required: true
|
||||
mask: true
|
||||
- name: vin
|
||||
example: W...
|
||||
- name: capacity
|
||||
- name: phases
|
||||
advanced: true
|
||||
- preset: vehicle-identify
|
||||
render: |
|
||||
type: tesla-proxy
|
||||
title: {{ .title }}
|
||||
icon: {{ .icon }}
|
||||
tokens:
|
||||
access: {{ .accessToken }}
|
||||
refresh: {{ .refreshToken }}
|
||||
capacity: {{ .capacity }}
|
||||
phases: {{ .phases }}
|
||||
vin: {{ .vin }}
|
||||
{{ include "vehicle-identify" . }}
|
||||
features: ["coarsecurrent"]
|
||||
|
|
@ -1,29 +1,18 @@
|
|||
template: tesla
|
||||
covers: ["tesla-command", "tesla-proxy"]
|
||||
products:
|
||||
- brand: Tesla
|
||||
description:
|
||||
de: veraltet
|
||||
en: legacy
|
||||
requirements:
|
||||
description:
|
||||
de: |
|
||||
Es wird ein `access` und ein `refresh` Token für die Kommunikation mit der Tesla API erstellt werden.
|
||||
|
||||
Folgende Apps ermöglichen das Erstellen von den beiden Tokens:
|
||||
- [Auth app for Tesla (iOS)](https://apps.apple.com/us/app/auth-app-for-tesla/id1552058613#?platform=iphone)
|
||||
- [Tesla Tokens (Android)](https://play.google.com/store/apps/details?id=net.leveugle.teslatokens)
|
||||
- [Tesla Auth (macOS, Linux)](https://github.com/adriankumpf/tesla_auth)
|
||||
|
||||
**Hinweis:** Diese API ist von Tesla [nicht offiziell unterstützt](https://tesla-api.timdorr.com) und wird Stück für Stück abgeschaltet.**
|
||||
Benötigt `access` und `refresh` Tokens. Diese können über [tesla.evcc.io](https://tesla.evcc.io) erstellt werden.
|
||||
Die Steuerung von Fahrzeugen im Zusammenspiel mit einem Tesla Wall Connector erfolgt über einen evcc Proxy-Server und benötigt ein Sponsor Token.
|
||||
Siehe https://tesla.evcc.io.
|
||||
en: |
|
||||
You need to generate an `access` and a `refresh` token for communicating with the Tesla API.
|
||||
|
||||
The following apps allow to create these tokens:
|
||||
- [Auth app for Tesla (iOS)](https://apps.apple.com/us/app/auth-app-for-tesla/id1552058613#?platform=iphone)
|
||||
- [Tesla Tokens (Android)](https://play.google.com/store/apps/details?id=net.leveugle.teslatokens)
|
||||
- [Tesla Auth (macOS, Linux)](https://github.com/adriankumpf/tesla_auth)
|
||||
|
||||
**Note:** This API is [not officially supported](https://tesla-api.timdorr.com) by Tesla and will be deprecated step by step.
|
||||
Teska `access` and `refresh` tokens are required. These can be generated through [tesla.evcc.io](https://tesla.evcc.io).
|
||||
Controlling vehicles in conjunction with a Tesla Wall Connector is done via an evcc proxy server and requires a sponsor token.
|
||||
Siehe https://tesla.evcc.io.
|
||||
evcc: ["sponsorship"]
|
||||
params:
|
||||
- name: title
|
||||
- name: icon
|
||||
|
|
@ -46,24 +35,25 @@ params:
|
|||
- name: capacity
|
||||
- name: phases
|
||||
advanced: true
|
||||
- name: control
|
||||
type: bool
|
||||
description:
|
||||
de: Fahrzeugsteuerung aktivieren
|
||||
en: Enable vehicle control
|
||||
help:
|
||||
de: Notwendig für Tesla Wall Connector. Der Virtual Key für evcc muss eingerichtet sein. Siehe https://tesla.evcc.io
|
||||
en: Use with Tesla Wall Connector. The Virtual Key for evcc must be installed. See https://tesla.evcc.io
|
||||
- preset: vehicle-identify
|
||||
render: |
|
||||
type: tesla
|
||||
{{- if .title }}
|
||||
title: {{ .title }}
|
||||
{{- end }}
|
||||
{{- if .icon }}
|
||||
icon: {{ .icon }}
|
||||
{{- end }}
|
||||
tokens:
|
||||
access: {{ .accessToken }}
|
||||
refresh: {{ .refreshToken }}
|
||||
capacity: {{ .capacity }}
|
||||
{{- if .phases }}
|
||||
phases: {{ .phases }}
|
||||
{{- end }}
|
||||
{{- if .vin }}
|
||||
control: {{ .control }}
|
||||
vin: {{ .vin }}
|
||||
{{- end }}
|
||||
{{ include "vehicle-identify" . }}
|
||||
features: ["coarsecurrent"]
|
||||
|
|
|
|||
|
|
@ -1,117 +0,0 @@
|
|||
package vehicle
|
||||
|
||||
import (
|
||||
"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"
|
||||
)
|
||||
|
||||
// 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: request.Timeout,
|
||||
Cache: interval,
|
||||
}
|
||||
|
||||
if err := util.DecodeOther(other, &cc); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
token, err := cc.Tokens.Token()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log := util.NewLogger("tesla-command").Redact(
|
||||
cc.Tokens.Access, cc.Tokens.Refresh,
|
||||
vc.OAuth2Config.ClientID, vc.OAuth2Config.ClientSecret,
|
||||
)
|
||||
|
||||
identity, err := vc.NewIdentity(log, token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
api := vc.NewAPI(log, identity, cc.Timeout)
|
||||
|
||||
// validate base url
|
||||
if _, err := api.Region(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
|
@ -1,258 +0,0 @@
|
|||
package vehicle
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"github.com/evcc-io/evcc/api"
|
||||
"github.com/evcc-io/evcc/provider"
|
||||
"github.com/evcc-io/evcc/util"
|
||||
"github.com/evcc-io/evcc/util/request"
|
||||
"github.com/evcc-io/evcc/util/sponsor"
|
||||
"github.com/evcc-io/evcc/util/transport"
|
||||
vc "github.com/evcc-io/evcc/vehicle/tesla-vehicle-command"
|
||||
tesla "github.com/evcc-io/tesla-proxy-client"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
// TeslaProxy is an api.Vehicle implementation for Tesla cars
|
||||
type TeslaProxy struct {
|
||||
*embed
|
||||
vehicle *tesla.Vehicle
|
||||
dataG func() (*tesla.VehicleData, error)
|
||||
}
|
||||
|
||||
func init() {
|
||||
registry.Add("tesla-proxy", NewTeslaProxyFromConfig)
|
||||
}
|
||||
|
||||
// NewTeslaProxyFromConfig creates a new vehicle
|
||||
func NewTeslaProxyFromConfig(other map[string]interface{}) (api.Vehicle, error) {
|
||||
cc := struct {
|
||||
embed `mapstructure:",squash"`
|
||||
Tokens Tokens
|
||||
URI string
|
||||
VIN string
|
||||
Cache time.Duration
|
||||
}{
|
||||
URI: "https://tesla.evcc.io/api/1",
|
||||
Cache: interval,
|
||||
}
|
||||
|
||||
if err := util.DecodeOther(other, &cc); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
token, err := cc.Tokens.Token()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !sponsor.IsAuthorized() {
|
||||
return nil, api.ErrSponsorRequired
|
||||
}
|
||||
|
||||
v := &TeslaProxy{
|
||||
embed: &cc.embed,
|
||||
}
|
||||
|
||||
// authenticated http client with logging injected to the Tesla client
|
||||
log := util.NewLogger("tesla-proxy").Redact(
|
||||
cc.Tokens.Access, cc.Tokens.Refresh,
|
||||
vc.OAuth2Config.ClientID, vc.OAuth2Config.ClientSecret,
|
||||
)
|
||||
|
||||
identity, err := vc.NewIdentity(log, token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
hc := request.NewClient(log)
|
||||
hc.Transport = &transport.Decorator{
|
||||
Decorator: transport.DecorateHeaders(map[string]string{
|
||||
"X-Auth-Token": sponsor.Token,
|
||||
}),
|
||||
Base: &oauth2.Transport{
|
||||
Source: identity,
|
||||
Base: hc.Transport,
|
||||
},
|
||||
}
|
||||
|
||||
options := []tesla.ClientOption{
|
||||
tesla.WithClient(hc),
|
||||
tesla.WithBaseURL(cc.URI),
|
||||
}
|
||||
|
||||
client, err := tesla.NewClient(context.Background(), options...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
v.vehicle, err = ensureVehicleEx(
|
||||
cc.VIN, client.Vehicles,
|
||||
func(v *tesla.Vehicle) string {
|
||||
return v.Vin
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if v.Title_ == "" {
|
||||
v.Title_ = v.vehicle.DisplayName
|
||||
}
|
||||
|
||||
v.dataG = provider.Cached(func() (*tesla.VehicleData, error) {
|
||||
res, err := v.vehicle.Data()
|
||||
return res, v.apiError(err)
|
||||
}, cc.Cache)
|
||||
|
||||
return v, nil
|
||||
}
|
||||
|
||||
// apiError converts HTTP 408 error to ErrTimeout
|
||||
func (v *TeslaProxy) apiError(err error) error {
|
||||
if err != nil && err.Error() == "408 Request Timeout" {
|
||||
err = api.ErrAsleep
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Soc implements the api.Vehicle interface
|
||||
func (v *TeslaProxy) Soc() (float64, error) {
|
||||
res, err := v.dataG()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return float64(res.Response.ChargeState.UsableBatteryLevel), nil
|
||||
}
|
||||
|
||||
var _ api.ChargeState = (*TeslaProxy)(nil)
|
||||
|
||||
// Status implements the api.ChargeState interface
|
||||
func (v *TeslaProxy) 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 = (*TeslaProxy)(nil)
|
||||
|
||||
// ChargedEnergy implements the api.ChargeRater interface
|
||||
func (v *TeslaProxy) ChargedEnergy() (float64, error) {
|
||||
res, err := v.dataG()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return res.Response.ChargeState.ChargeEnergyAdded, nil
|
||||
}
|
||||
|
||||
var _ api.VehicleRange = (*TeslaProxy)(nil)
|
||||
|
||||
// Range implements the api.VehicleRange interface
|
||||
func (v *TeslaProxy) 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 = (*TeslaProxy)(nil)
|
||||
|
||||
// Odometer implements the api.VehicleOdometer interface
|
||||
func (v *TeslaProxy) 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 = (*TeslaProxy)(nil)
|
||||
|
||||
// FinishTime implements the api.VehicleFinishTimer interface
|
||||
func (v *TeslaProxy) 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 = (*TeslaProxy)(nil)
|
||||
|
||||
// Position implements the api.VehiclePosition interface
|
||||
func (v *TeslaProxy) Position() (float64, float64, error) {
|
||||
res, err := v.dataG()
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
return res.Response.DriveState.Latitude, res.Response.DriveState.Longitude, nil
|
||||
}
|
||||
|
||||
var _ api.SocLimiter = (*TeslaProxy)(nil)
|
||||
|
||||
// TargetSoc implements the api.SocLimiter interface
|
||||
func (v *TeslaProxy) TargetSoc() (float64, error) {
|
||||
res, err := v.dataG()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return float64(res.Response.ChargeState.ChargeLimitSoc), nil
|
||||
}
|
||||
|
||||
var _ api.CurrentController = (*TeslaProxy)(nil)
|
||||
|
||||
// MaxCurrent implements the api.CurrentLimiter interface
|
||||
func (v *TeslaProxy) MaxCurrent(current int64) error {
|
||||
return v.apiError(v.vehicle.SetChargingAmps(int(current)))
|
||||
}
|
||||
|
||||
var _ api.Resurrector = (*TeslaProxy)(nil)
|
||||
|
||||
// WakeUp implements the api.Resurrector interface
|
||||
func (v *TeslaProxy) WakeUp() error {
|
||||
_, err := v.vehicle.Wakeup()
|
||||
return v.apiError(err)
|
||||
}
|
||||
|
||||
var _ api.VehicleChargeController = (*TeslaProxy)(nil)
|
||||
|
||||
// StartCharge implements the api.VehicleChargeController interface
|
||||
func (v *TeslaProxy) StartCharge() error {
|
||||
err := v.apiError(v.vehicle.StartCharging())
|
||||
if err != nil && slices.Contains([]string{"complete", "is_charging"}, err.Error()) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// StopCharge implements the api.VehicleChargeController interface
|
||||
func (v *TeslaProxy) StopCharge() error {
|
||||
err := v.apiError(v.vehicle.StopCharging())
|
||||
|
||||
// ignore sleeping vehicle
|
||||
if errors.Is(err, api.ErrAsleep) {
|
||||
err = nil
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
|
@ -1,105 +0,0 @@
|
|||
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
|
||||
base string
|
||||
}
|
||||
|
||||
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,
|
||||
base: FleetAudienceEU,
|
||||
}
|
||||
}
|
||||
|
||||
func (v *API) Region() (Region, error) {
|
||||
var res RegionResponse
|
||||
err := v.GetJSON(fmt.Sprintf("%s/api/1/users/region", FleetAudienceEU), &res)
|
||||
if err == nil {
|
||||
v.base = res.Response.FleetApiBaseUrl
|
||||
}
|
||||
return res.Response, err
|
||||
}
|
||||
|
||||
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", v.base), &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", v.base, id), &res)
|
||||
|
||||
return &res, err
|
||||
}
|
||||
|
||||
func (v *API) WakeUp(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/wake_up", v.base, id), &res)
|
||||
|
||||
return &res, err
|
||||
}
|
||||
268
vehicle/tesla.go
268
vehicle/tesla.go
|
|
@ -2,38 +2,55 @@ package vehicle
|
|||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"slices"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/bogosj/tesla"
|
||||
"github.com/evcc-io/evcc/api"
|
||||
"github.com/evcc-io/evcc/provider"
|
||||
"github.com/evcc-io/evcc/util"
|
||||
"github.com/evcc-io/evcc/util/request"
|
||||
"github.com/evcc-io/evcc/util/sponsor"
|
||||
"github.com/evcc-io/evcc/util/transport"
|
||||
"github.com/evcc-io/evcc/vehicle/tesla"
|
||||
teslaclient "github.com/evcc-io/tesla-proxy-client"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
// Tesla is an api.Vehicle implementation for Tesla cars
|
||||
// Tesla is an api.Vehicle implementation for Tesla cars using the official Tesla vehicle-command api.
|
||||
type Tesla struct {
|
||||
*embed
|
||||
vehicle *tesla.Vehicle
|
||||
dataG func() (*tesla.VehicleData, error)
|
||||
*tesla.Provider
|
||||
}
|
||||
|
||||
type teslaController struct {
|
||||
*embed
|
||||
*tesla.Provider
|
||||
*tesla.Controller
|
||||
}
|
||||
|
||||
func init() {
|
||||
registry.Add("tesla", NewTeslaFromConfig)
|
||||
if id := os.Getenv("TESLA_CLIENT_ID"); id != "" {
|
||||
tesla.OAuth2Config.ClientID = id
|
||||
}
|
||||
if secret := os.Getenv("TESLA_CLIENT_SECRET"); secret != "" {
|
||||
tesla.OAuth2Config.ClientSecret = secret
|
||||
}
|
||||
if tesla.OAuth2Config.ClientID != "" {
|
||||
registry.Add("tesla", NewTeslaFromConfig)
|
||||
}
|
||||
}
|
||||
|
||||
// NewTeslaFromConfig creates a new vehicle
|
||||
func NewTeslaFromConfig(other map[string]interface{}) (api.Vehicle, error) {
|
||||
cc := struct {
|
||||
embed `mapstructure:",squash"`
|
||||
Tokens Tokens
|
||||
VIN string
|
||||
Cache time.Duration
|
||||
embed `mapstructure:",squash"`
|
||||
Tokens Tokens
|
||||
VIN string
|
||||
Control bool
|
||||
Timeout time.Duration
|
||||
Cache time.Duration
|
||||
}{
|
||||
Cache: interval,
|
||||
Timeout: request.Timeout,
|
||||
Cache: interval,
|
||||
}
|
||||
|
||||
if err := util.DecodeOther(other, &cc); err != nil {
|
||||
|
|
@ -45,183 +62,84 @@ func NewTeslaFromConfig(other map[string]interface{}) (api.Vehicle, error) {
|
|||
return nil, err
|
||||
}
|
||||
|
||||
v := &Tesla{
|
||||
embed: &cc.embed,
|
||||
}
|
||||
log := util.NewLogger("tesla").Redact(
|
||||
cc.Tokens.Access, cc.Tokens.Refresh,
|
||||
tesla.OAuth2Config.ClientID, tesla.OAuth2Config.ClientSecret,
|
||||
)
|
||||
|
||||
// authenticated http client with logging injected to the Tesla client
|
||||
log := util.NewLogger("tesla").Redact(cc.Tokens.Access, cc.Tokens.Refresh)
|
||||
ctx := context.WithValue(context.Background(), oauth2.HTTPClient, request.NewClient(log))
|
||||
|
||||
client, err := tesla.NewClient(ctx, tesla.WithToken(token))
|
||||
identity, err := tesla.NewIdentity(log, token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
v.vehicle, err = ensureVehicleEx(
|
||||
cc.VIN, client.Vehicles,
|
||||
hc := request.NewClient(log)
|
||||
hc.Transport = &oauth2.Transport{
|
||||
Source: identity,
|
||||
Base: hc.Transport,
|
||||
}
|
||||
|
||||
tc, err := teslaclient.NewClient(context.Background(), teslaclient.WithClient(hc))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// validate base url
|
||||
region, err := tc.UserRegion()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tc.SetBaseUrl(region.FleetApiBaseUrl)
|
||||
|
||||
vehicle, err := ensureVehicleEx(
|
||||
cc.VIN, tc.Vehicles,
|
||||
func(v *tesla.Vehicle) string {
|
||||
return v.Vin
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if v.Title_ == "" {
|
||||
v.Title_ = v.vehicle.DisplayName
|
||||
v := &Tesla{
|
||||
embed: &cc.embed,
|
||||
Provider: tesla.NewProvider(vehicle, cc.Cache),
|
||||
}
|
||||
|
||||
v.dataG = provider.Cached(func() (*tesla.VehicleData, error) {
|
||||
res, err := v.vehicle.Data()
|
||||
return res, v.apiError(err)
|
||||
}, cc.Cache)
|
||||
if v.Title_ == "" {
|
||||
v.Title_ = vehicle.DisplayName
|
||||
}
|
||||
|
||||
if cc.Control {
|
||||
if !sponsor.IsAuthorized() {
|
||||
return nil, api.ErrSponsorRequired
|
||||
}
|
||||
|
||||
// proxy client
|
||||
pc := request.NewClient(log)
|
||||
pc.Transport = &transport.Decorator{
|
||||
Decorator: transport.DecorateHeaders(map[string]string{
|
||||
"X-Auth-Token": sponsor.Token,
|
||||
}),
|
||||
Base: hc.Transport,
|
||||
}
|
||||
|
||||
tc, err := teslaclient.NewClient(context.Background(), teslaclient.WithClient(pc))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tc.SetBaseUrl(tesla.ProxyBaseUrl)
|
||||
|
||||
vehicle, err := tc.Vehicle(vehicle.Vin)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &teslaController{
|
||||
embed: v.embed,
|
||||
Provider: v.Provider,
|
||||
Controller: tesla.NewController(vehicle),
|
||||
}, nil
|
||||
}
|
||||
|
||||
return v, nil
|
||||
}
|
||||
|
||||
// apiError converts HTTP 408 error to ErrTimeout
|
||||
func (v *Tesla) apiError(err error) error {
|
||||
if err != nil && err.Error() == "408 Request Timeout" {
|
||||
err = api.ErrAsleep
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Soc implements the api.Vehicle interface
|
||||
func (v *Tesla) Soc() (float64, error) {
|
||||
res, err := v.dataG()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return float64(res.Response.ChargeState.UsableBatteryLevel), nil
|
||||
}
|
||||
|
||||
var _ api.ChargeState = (*Tesla)(nil)
|
||||
|
||||
// Status implements the api.ChargeState interface
|
||||
func (v *Tesla) 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 = (*Tesla)(nil)
|
||||
|
||||
// ChargedEnergy implements the api.ChargeRater interface
|
||||
func (v *Tesla) ChargedEnergy() (float64, error) {
|
||||
res, err := v.dataG()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return res.Response.ChargeState.ChargeEnergyAdded, nil
|
||||
}
|
||||
|
||||
var _ api.VehicleRange = (*Tesla)(nil)
|
||||
|
||||
// Range implements the api.VehicleRange interface
|
||||
func (v *Tesla) 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 = (*Tesla)(nil)
|
||||
|
||||
// Odometer implements the api.VehicleOdometer interface
|
||||
func (v *Tesla) 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 = (*Tesla)(nil)
|
||||
|
||||
// FinishTime implements the api.VehicleFinishTimer interface
|
||||
func (v *Tesla) 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 = (*Tesla)(nil)
|
||||
|
||||
// Position implements the api.VehiclePosition interface
|
||||
func (v *Tesla) Position() (float64, float64, error) {
|
||||
res, err := v.dataG()
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
return res.Response.DriveState.Latitude, res.Response.DriveState.Longitude, nil
|
||||
}
|
||||
|
||||
var _ api.SocLimiter = (*Tesla)(nil)
|
||||
|
||||
// TargetSoc implements the api.SocLimiter interface
|
||||
func (v *Tesla) TargetSoc() (float64, error) {
|
||||
res, err := v.dataG()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return float64(res.Response.ChargeState.ChargeLimitSoc), nil
|
||||
}
|
||||
|
||||
var _ api.CurrentController = (*Tesla)(nil)
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
var _ api.VehicleChargeController = (*Tesla)(nil)
|
||||
|
||||
// StartCharge implements the api.VehicleChargeController interface
|
||||
func (v *Tesla) StartCharge() error {
|
||||
err := v.apiError(v.vehicle.StartCharging())
|
||||
if err != nil && slices.Contains([]string{"complete", "is_charging"}, err.Error()) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// StopCharge implements the api.VehicleChargeController interface
|
||||
func (v *Tesla) StopCharge() error {
|
||||
err := v.apiError(v.vehicle.StopCharging())
|
||||
|
||||
// ignore sleeping vehicle
|
||||
if errors.Is(err, api.ErrAsleep) {
|
||||
err = nil
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
53
vehicle/tesla/controller.go
Normal file
53
vehicle/tesla/controller.go
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
package tesla
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"slices"
|
||||
|
||||
"github.com/evcc-io/evcc/api"
|
||||
"github.com/evcc-io/tesla-proxy-client"
|
||||
)
|
||||
|
||||
const ProxyBaseUrl = "https://tesla.evcc.io"
|
||||
|
||||
type Controller struct {
|
||||
vehicle *tesla.Vehicle
|
||||
}
|
||||
|
||||
// NewController creates a vehicle current and charge controller
|
||||
func NewController(vehicle *tesla.Vehicle) *Controller {
|
||||
impl := &Controller{
|
||||
vehicle: vehicle,
|
||||
}
|
||||
return impl
|
||||
}
|
||||
|
||||
var _ api.CurrentController = (*Controller)(nil)
|
||||
|
||||
// StartCharge implements the api.VehicleChargeController interface
|
||||
func (v *Controller) MaxCurrent(current int64) error {
|
||||
return apiError(v.vehicle.SetChargingAmps(int(current)))
|
||||
}
|
||||
|
||||
var _ api.VehicleChargeController = (*Controller)(nil)
|
||||
|
||||
// StartCharge implements the api.VehicleChargeController interface
|
||||
func (v *Controller) StartCharge() error {
|
||||
err := apiError(v.vehicle.StartCharging())
|
||||
if err != nil && slices.Contains([]string{"complete", "is_charging"}, err.Error()) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// StopCharge implements the api.VehicleChargeController interface
|
||||
func (v *Controller) StopCharge() error {
|
||||
err := apiError(v.vehicle.StopCharging())
|
||||
|
||||
// ignore sleeping vehicle
|
||||
if errors.Is(err, api.ErrAsleep) {
|
||||
err = nil
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package vc
|
||||
package tesla
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
|
@ -15,8 +15,7 @@ var (
|
|||
)
|
||||
|
||||
func getInstance(subject string) *Identity {
|
||||
v, _ := identities[subject]
|
||||
return v
|
||||
return identities[subject]
|
||||
}
|
||||
|
||||
func addInstance(subject string, identity *Identity) {
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package vc
|
||||
package tesla
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
|
@ -27,8 +27,6 @@ var OAuth2Config = &oauth2.Config{
|
|||
Scopes: []string{"openid", "email", "offline_access"},
|
||||
}
|
||||
|
||||
// const userAgent = "evcc/evcc-io"
|
||||
|
||||
var TESLA_CLIENT_ID, TESLA_CLIENT_SECRET string
|
||||
|
||||
func init() {
|
||||
|
|
@ -45,10 +43,9 @@ type Identity struct {
|
|||
mu sync.Mutex
|
||||
log *util.Logger
|
||||
subject string
|
||||
// acct *account.Account
|
||||
}
|
||||
|
||||
func NewIdentity(log *util.Logger, token *oauth2.Token) (*Identity, error) {
|
||||
func NewIdentity(log *util.Logger, token *oauth2.Token) (oauth2.TokenSource, error) {
|
||||
// serialise instance handling
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
|
|
@ -71,7 +68,6 @@ func NewIdentity(log *util.Logger, token *oauth2.Token) (*Identity, error) {
|
|||
v := &Identity{
|
||||
log: log,
|
||||
subject: claims.Subject,
|
||||
// acct: acct,
|
||||
}
|
||||
|
||||
// database token
|
||||
|
|
@ -92,11 +88,6 @@ func NewIdentity(log *util.Logger, token *oauth2.Token) (*Identity, error) {
|
|||
return nil, errors.New("token expired")
|
||||
}
|
||||
|
||||
// acct, err := account.New(token.AccessToken, userAgent)
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
|
||||
v.TokenSource = oauth.RefreshTokenSource(token, v)
|
||||
|
||||
// add instance
|
||||
|
|
@ -125,23 +116,3 @@ func (v *Identity) RefreshToken(token *oauth2.Token) (*oauth2.Token, error) {
|
|||
|
||||
return token, err
|
||||
}
|
||||
|
||||
// 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
|
||||
// }
|
||||
|
|
@ -1,25 +1,26 @@
|
|||
package vc
|
||||
package tesla
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/evcc-io/evcc/api"
|
||||
"github.com/evcc-io/evcc/provider"
|
||||
"github.com/evcc-io/tesla-proxy-client"
|
||||
)
|
||||
|
||||
type Provider struct {
|
||||
dataG func() (*VehicleData, error)
|
||||
wakeup func() (*VehicleData, error)
|
||||
dataG func() (*tesla.VehicleData, error)
|
||||
wakeup func() (*tesla.Vehicle, error)
|
||||
}
|
||||
|
||||
func NewProvider(api *API, vid int64, cache time.Duration) *Provider {
|
||||
func NewProvider(vehicle *tesla.Vehicle, cache time.Duration) *Provider {
|
||||
impl := &Provider{
|
||||
dataG: provider.Cached(func() (*VehicleData, error) {
|
||||
res, err := api.VehicleData(vid)
|
||||
dataG: provider.Cached(func() (*tesla.VehicleData, error) {
|
||||
res, err := vehicle.Data()
|
||||
return res, apiError(err)
|
||||
}, cache),
|
||||
wakeup: func() (*VehicleData, error) {
|
||||
res, err := api.WakeUp(vid)
|
||||
wakeup: func() (*tesla.Vehicle, error) {
|
||||
res, err := vehicle.Wakeup()
|
||||
return res, apiError(err)
|
||||
},
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package vc
|
||||
package tesla
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
package vc
|
||||
package tesla
|
||||
|
||||
import "github.com/bogosj/tesla"
|
||||
import (
|
||||
tesla "github.com/evcc-io/tesla-proxy-client"
|
||||
)
|
||||
|
||||
type (
|
||||
Vehicle = tesla.Vehicle
|
||||
|
|
@ -7,8 +7,6 @@ import (
|
|||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
const kmPerMile = 1.609344
|
||||
|
||||
// ClientCredentials contains OAuth2 client id and secret
|
||||
type ClientCredentials struct {
|
||||
ID, Secret string
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue