Add Tesla proxy client (#12174)
This commit is contained in:
parent
50734bbe14
commit
777cb7b7bc
6 changed files with 324 additions and 17 deletions
11
go.mod
11
go.mod
|
|
@ -30,6 +30,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/fatih/structs v1.1.0
|
||||
github.com/glebarez/sqlite v1.10.0
|
||||
github.com/go-http-utils/etag v0.0.0-20161124023236-513ea8f21eb1
|
||||
|
|
@ -95,8 +96,8 @@ require (
|
|||
go.uber.org/mock v0.4.0
|
||||
golang.org/x/crypto/x509roots/fallback v0.0.0-20240108164429-dbb6ec16ecef
|
||||
golang.org/x/exp v0.0.0-20240119083558-1b970713d09a
|
||||
golang.org/x/net v0.20.0
|
||||
golang.org/x/oauth2 v0.16.0
|
||||
golang.org/x/net v0.21.0
|
||||
golang.org/x/oauth2 v0.17.0
|
||||
golang.org/x/sync v0.6.0
|
||||
golang.org/x/text v0.14.0
|
||||
google.golang.org/grpc v1.61.0
|
||||
|
|
@ -187,10 +188,10 @@ require (
|
|||
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
|
||||
gitlab.com/c0b/go-ordered-json v0.0.0-20201030195603-febf46534d5a // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
golang.org/x/crypto v0.18.0 // indirect
|
||||
golang.org/x/crypto v0.19.0 // indirect
|
||||
golang.org/x/mod v0.14.0 // indirect
|
||||
golang.org/x/sys v0.16.0 // indirect
|
||||
golang.org/x/term v0.16.0 // indirect
|
||||
golang.org/x/sys v0.17.0 // indirect
|
||||
golang.org/x/term v0.17.0 // indirect
|
||||
golang.org/x/tools v0.17.0 // indirect
|
||||
google.golang.org/appengine v1.6.8 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240125205218-1f4bbc51befe // indirect
|
||||
|
|
|
|||
22
go.sum
22
go.sum
|
|
@ -143,6 +143,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/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=
|
||||
|
|
@ -735,8 +737,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
|
|||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
|
||||
golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc=
|
||||
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
|
||||
golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto/x509roots/fallback v0.0.0-20240108164429-dbb6ec16ecef h1:vEvmmVshwWdgDSUnJQjVouHPkUqPmPX0l4yhjzxsrgE=
|
||||
golang.org/x/crypto/x509roots/fallback v0.0.0-20240108164429-dbb6ec16ecef/go.mod h1:kNa9WdvYnzFwC79zRpLRMJbdEFlhyM5RPFBBZp/wWH8=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
|
|
@ -785,12 +787,12 @@ golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
|
|||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
||||
golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo=
|
||||
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
|
||||
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.16.0 h1:aDkGMBSYxElaoP81NpoUoz2oo2R2wHdZpGToUxfyQrQ=
|
||||
golang.org/x/oauth2 v0.16.0/go.mod h1:hqZ+0LWXsiVoZpeld6jVt06P3adbS2Uu911W1SsJv2o=
|
||||
golang.org/x/oauth2 v0.17.0 h1:6m3ZPmLEFdVxKKWnKq4VqZ60gutO35zm+zrAHVmHyDQ=
|
||||
golang.org/x/oauth2 v0.17.0/go.mod h1:OzPDGQiuQMguemayvdylqddI7qcD9lnSDb+1FiwQ5HA=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
|
|
@ -851,15 +853,15 @@ golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
|
||||
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
|
||||
golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE=
|
||||
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
|
||||
golang.org/x/term v0.17.0 h1:mkTF7LCd6WGJNL3K1Ad7kwxNfYAW6a8a8QqtMblp/4U=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
|
|
|
|||
46
templates/definition/vehicle/tesla-proxy.yaml
Normal file
46
templates/definition/vehicle/tesla-proxy.yaml
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
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"]
|
||||
258
vehicle/tesla-proxy.go
Normal file
258
vehicle/tesla-proxy.go
Normal file
|
|
@ -0,0 +1,258 @@
|
|||
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
|
||||
}
|
||||
|
|
@ -129,8 +129,6 @@ func (v *Tesla) ChargedEnergy() (float64, error) {
|
|||
return res.Response.ChargeState.ChargeEnergyAdded, nil
|
||||
}
|
||||
|
||||
const kmPerMile = 1.609344
|
||||
|
||||
var _ api.VehicleRange = (*Tesla)(nil)
|
||||
|
||||
// Range implements the api.VehicleRange interface
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ 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