Add BMW i3 and Nissan Leaf (#21)

This commit is contained in:
andig 2020-04-04 21:00:36 +02:00 • committed by GitHub
parent 4dc9984358
commit 7ab24bd8a5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 257 additions and 10 deletions

1
go.mod
View file

@ -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
View file

@ -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=

View file

@ -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
View 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()
}

View file

@ -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
View 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()
}

View file

@ -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