Add BMW i3 and Nissan Leaf (#21)
This commit is contained in:
parent
4dc9984358
commit
7ab24bd8a5
7 changed files with 257 additions and 10 deletions
1
go.mod
1
go.mod
|
|
@ -19,6 +19,7 @@ require (
|
|||
github.com/grid-x/serial v0.0.0-20191104121038-e24bc9bf6f08 // indirect
|
||||
github.com/hashicorp/go-version v1.2.0 // indirect
|
||||
github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d
|
||||
github.com/joeshaw/carwings v0.0.0-20191118152321-61b46581307a // indirect
|
||||
github.com/jsgoecke/tesla v0.0.0-20190206234002-112508e1374e
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
|
||||
github.com/mitchellh/mapstructure v1.2.2
|
||||
|
|
|
|||
4
go.sum
4
go.sum
|
|
@ -135,6 +135,8 @@ github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NH
|
|||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d h1:/WZQPMZNsjZ7IlCpsLGdQBINg5bxKQ1K1sh6awxLtkA=
|
||||
github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo=
|
||||
github.com/joeshaw/carwings v0.0.0-20191118152321-61b46581307a h1:wOlcxK/k5rPhtMyEFVayqVh0x3lWvnyEaK8Aw96zx4M=
|
||||
github.com/joeshaw/carwings v0.0.0-20191118152321-61b46581307a/go.mod h1:tB0OlpicmRVTL1Vksc5XRiYo+wkK2kl/GI7eGMIl6Rs=
|
||||
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
|
||||
github.com/jsgoecke/tesla v0.0.0-20190206234002-112508e1374e h1:IONO/IEnmILgcxbjiXKauCsm+47xnP3FHUlsaPkVTC4=
|
||||
github.com/jsgoecke/tesla v0.0.0-20190206234002-112508e1374e/go.mod h1:GlLJU/C6KA3FEAIZTFDvIDlNGE1+YlKGHHXdNVkt5ow=
|
||||
|
|
@ -182,6 +184,7 @@ github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181
|
|||
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||
github.com/pelletier/go-toml v1.6.0 h1:aetoXYr0Tv7xRU/V4B4IZJ2QcbtMUFoNb3ORp7TzIK4=
|
||||
github.com/pelletier/go-toml v1.6.0/go.mod h1:5N711Q9dKgbdkxHL+MEfF31hpT7l0S0s/t2kKREewys=
|
||||
github.com/peterbourgon/ff v1.2.0/go.mod h1:ljiF7yxtUvZaxUDyUqQa0+uiEOgwVboj+Q2S2+0nq40=
|
||||
github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
|
|
@ -257,6 +260,7 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90Pveol
|
|||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 h1:ObdrDkeb4kJdCP557AjRjq69pTHfNouLtWZG7j9rPN8=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
|
|
|
|||
|
|
@ -15,10 +15,9 @@ import (
|
|||
)
|
||||
|
||||
const (
|
||||
audiURL = "https://msg.audi.de/fs-car"
|
||||
audiDE = "Audi/DE"
|
||||
audiAuthPrefix = "AudiAuth 1"
|
||||
audiValidMargin = 10 * time.Second
|
||||
audiURL = "https://msg.audi.de/fs-car"
|
||||
audiDE = "Audi/DE"
|
||||
audiAuthPrefix = "AudiAuth 1"
|
||||
)
|
||||
|
||||
// Audi is an api.Vehicle implementation for Audi cars
|
||||
|
|
@ -27,7 +26,6 @@ type Audi struct {
|
|||
user, password, vin string
|
||||
token string
|
||||
tokenValid time.Time
|
||||
cache time.Duration
|
||||
chargeStateG provider.FloatGetter
|
||||
}
|
||||
|
||||
|
|
@ -36,6 +34,7 @@ type audiTokenResponse struct {
|
|||
TokenType string `json:"token_type"`
|
||||
ExpiresIn int `json:"expires_in"`
|
||||
}
|
||||
|
||||
type audiErrorResponse struct {
|
||||
Error string
|
||||
Description string `json:"error_description"`
|
||||
|
|
@ -76,7 +75,6 @@ func NewAudiFromConfig(log *api.Logger, other map[string]interface{}) api.Vehicl
|
|||
user: cc.User,
|
||||
password: cc.Password,
|
||||
vin: cc.VIN,
|
||||
cache: cc.Cache,
|
||||
}
|
||||
|
||||
v.chargeStateG = provider.NewCached(v.chargeState, cc.Cache).FloatGetter()
|
||||
|
|
@ -150,7 +148,7 @@ func (v *Audi) login(user, password string) error {
|
|||
}
|
||||
|
||||
v.token = tr.AccessToken
|
||||
v.tokenValid = time.Now().Add(time.Duration(tr.ExpiresIn)*time.Second - audiValidMargin)
|
||||
v.tokenValid = time.Now().Add(time.Duration(tr.ExpiresIn)*time.Second - tokenValidMargin)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
174
vehicle/bmw.go
Normal file
174
vehicle/bmw.go
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
package vehicle
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/andig/evcc/api"
|
||||
"github.com/andig/evcc/provider"
|
||||
)
|
||||
|
||||
const (
|
||||
bmwURL = "https://b2vapi.bmwgroup.com/webapi"
|
||||
)
|
||||
|
||||
type bmwTokenResponse struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
TokenType string `json:"token_type"`
|
||||
ExpiresIn int `json:"expires_in"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
Scope string `json:"scope"`
|
||||
}
|
||||
|
||||
type bmwStatusResponse struct {
|
||||
VehicleStatus bmwVehicleStatus `json:"vehicleStatus"`
|
||||
}
|
||||
|
||||
type bmwVehicleStatus struct {
|
||||
ConnectionStatus string `json:"connectionStatus"`
|
||||
ChargingStatus string `json:"chargingStatus"`
|
||||
ChargingLevelHv int `json:"chargingLevelHv"`
|
||||
ChargingTimeRemaining int `json:"chargingTimeRemaining"`
|
||||
}
|
||||
|
||||
// BMW is an api.Vehicle implementation for BMW cars
|
||||
type BMW struct {
|
||||
*embed
|
||||
user, password, vin string
|
||||
token, refreshToken string
|
||||
tokenValid time.Time
|
||||
chargeStateG provider.FloatGetter
|
||||
}
|
||||
|
||||
// NewBMWFromConfig creates a new vehicle
|
||||
func NewBMWFromConfig(log *api.Logger, other map[string]interface{}) api.Vehicle {
|
||||
cc := struct {
|
||||
Title string
|
||||
Capacity int64
|
||||
User, Password, VIN string
|
||||
Cache time.Duration
|
||||
}{}
|
||||
api.DecodeOther(log, other, &cc)
|
||||
|
||||
v := &BMW{
|
||||
embed: &embed{cc.Title, cc.Capacity},
|
||||
user: cc.User,
|
||||
password: cc.Password,
|
||||
vin: cc.VIN,
|
||||
}
|
||||
|
||||
v.chargeStateG = provider.NewCached(v.chargeState, cc.Cache).FloatGetter()
|
||||
|
||||
return v
|
||||
}
|
||||
|
||||
func (v *BMW) apiURL(service string) string {
|
||||
return fmt.Sprintf("%s/%s", bmwURL, service)
|
||||
}
|
||||
|
||||
func (v *BMW) authHeader() string {
|
||||
token := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", v.user, v.password)))
|
||||
return fmt.Sprintf("Basic %s", token)
|
||||
}
|
||||
|
||||
func (v *BMW) login(user, password string) error {
|
||||
uri := v.apiURL("oauth/token")
|
||||
|
||||
data := url.Values{
|
||||
"grant_type": []string{"password"},
|
||||
"username": []string{user},
|
||||
"password": []string{password},
|
||||
"scope": []string{"remote_services vehicle_data"},
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(http.MethodPost, uri, strings.NewReader(data.Encode()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", v.authHeader())
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
b, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("unexpected response %d: %s", resp.StatusCode, string(b))
|
||||
}
|
||||
|
||||
var tr bmwTokenResponse
|
||||
if err = json.Unmarshal(b, &tr); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
v.token = tr.AccessToken
|
||||
v.refreshToken = tr.RefreshToken
|
||||
v.tokenValid = time.Now().Add(time.Duration(tr.ExpiresIn)*time.Second - tokenValidMargin)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// @TODO implement refresh_token
|
||||
func (v *BMW) request(uri string) (*http.Request, error) {
|
||||
// token invalid or expired
|
||||
if v.token == "" || time.Now().After(v.tokenValid) {
|
||||
if err := v.login(v.user, v.password); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, uri, nil)
|
||||
if err != nil {
|
||||
return req, err
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", v.token))
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
// chargeState implements the Vehicle.ChargeState interface
|
||||
func (v *BMW) chargeState() (float64, error) {
|
||||
uri := v.apiURL(fmt.Sprintf("v1/user/vehicles/%s/status", v.vin))
|
||||
req, err := v.request(uri)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
b, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
var br bmwStatusResponse
|
||||
err = json.Unmarshal(b, &br)
|
||||
|
||||
return float64(br.VehicleStatus.ChargingLevelHv), err
|
||||
}
|
||||
|
||||
// ChargeState implements the Vehicle.ChargeState interface
|
||||
func (v *BMW) ChargeState() (float64, error) {
|
||||
return v.chargeStateG()
|
||||
}
|
||||
|
|
@ -6,17 +6,21 @@ import (
|
|||
"github.com/andig/evcc/api"
|
||||
)
|
||||
|
||||
// NewFromConfig creates charger from configuration
|
||||
// NewFromConfig creates vehicle from configuration
|
||||
func NewFromConfig(log *api.Logger, typ string, other map[string]interface{}) api.Vehicle {
|
||||
var c api.Vehicle
|
||||
|
||||
switch strings.ToLower(typ) {
|
||||
case "script", "exec":
|
||||
c = NewConfigurableFromConfig(log, other)
|
||||
case "audi":
|
||||
case "audi", "etron":
|
||||
c = NewAudiFromConfig(log, other)
|
||||
case "tesla":
|
||||
case "bmw", "i3":
|
||||
c = NewBMWFromConfig(log, other)
|
||||
case "tesla", "model3", "model 3", "models", "model s":
|
||||
c = NewTeslaFromConfig(log, other)
|
||||
case "nissan", "leaf":
|
||||
c = NewNissanFromConfig(log, other)
|
||||
default:
|
||||
log.FATAL.Fatalf("invalid vehicle type '%s'", typ)
|
||||
}
|
||||
|
|
|
|||
60
vehicle/nissan.go
Normal file
60
vehicle/nissan.go
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
package vehicle
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/andig/evcc/api"
|
||||
"github.com/andig/evcc/provider"
|
||||
|
||||
"github.com/joeshaw/carwings"
|
||||
)
|
||||
|
||||
// Nissan is an api.Vehicle implementation for Nissan cars
|
||||
type Nissan struct {
|
||||
*embed
|
||||
session *carwings.Session
|
||||
chargeStateG provider.FloatGetter
|
||||
}
|
||||
|
||||
// NewNissanFromConfig creates a new vehicle
|
||||
func NewNissanFromConfig(log *api.Logger, other map[string]interface{}) api.Vehicle {
|
||||
cc := struct {
|
||||
Title string
|
||||
Capacity int64
|
||||
User, Password, Region string
|
||||
Cache time.Duration
|
||||
}{}
|
||||
api.DecodeOther(log, other, &cc)
|
||||
|
||||
if cc.Region == "" {
|
||||
cc.Region = carwings.RegionEurope
|
||||
}
|
||||
|
||||
session := &carwings.Session{
|
||||
Region: cc.Region,
|
||||
}
|
||||
|
||||
if err := session.Connect(cc.User, cc.Password); err != nil {
|
||||
log.FATAL.Fatalf("cannot create nissan: %v", err)
|
||||
}
|
||||
|
||||
v := &Nissan{
|
||||
embed: &embed{cc.Title, cc.Capacity},
|
||||
session: session,
|
||||
}
|
||||
|
||||
v.chargeStateG = provider.NewCached(v.chargeState, cc.Cache).FloatGetter()
|
||||
|
||||
return v
|
||||
}
|
||||
|
||||
// chargeState implements the Vehicle.ChargeState interface
|
||||
func (v *Nissan) chargeState() (float64, error) {
|
||||
bs, err := v.session.BatteryStatus()
|
||||
return float64(bs.StateOfCharge), err
|
||||
}
|
||||
|
||||
// ChargeState implements the Vehicle.ChargeState interface
|
||||
func (v *Nissan) ChargeState() (float64, error) {
|
||||
return v.chargeStateG()
|
||||
}
|
||||
|
|
@ -1,10 +1,16 @@
|
|||
package vehicle
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/andig/evcc/api"
|
||||
"github.com/andig/evcc/provider"
|
||||
)
|
||||
|
||||
const (
|
||||
tokenValidMargin = 10 * time.Second // safety margin for api token validity
|
||||
)
|
||||
|
||||
type embed struct {
|
||||
title string
|
||||
capacity int64
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue