From ef6140cce507fa8565dfe0aaada629c620a3c8c6 Mon Sep 17 00:00:00 2001 From: andig Date: Sun, 29 Mar 2020 21:15:44 +0200 Subject: [PATCH] Support Audi and Tesla vehicle APIs (#20) --- Dockerfile | 8 +- README.md | 14 ++- api/api.go | 6 +- {charger => api}/decoder.go | 6 +- charger/charger.go | 2 +- charger/go-e.go | 2 +- charger/nrgkick.go | 2 +- charger/phoenix.go | 2 +- charger/simpleevse.go | 2 +- charger/wallbe.go | 2 +- cmd/config.go | 25 ++--- cmd/setup.go | 20 ++-- core/api.go | 30 +++--- core/loadpoint.go | 10 +- core/soc.go | 27 ----- go.mod | 1 + go.sum | 2 + modules/audi/audi.py | 21 ---- modules/audi/audi.sh | 13 --- tmpl.Dockerfile | 12 +-- vehicle/audi.go | 208 ++++++++++++++++++++++++++++++++++++ vehicle/config.go | 23 ++++ vehicle/tesla.go | 84 +++++++++++++++ vehicle/vehicle.go | 47 ++++++++ 24 files changed, 432 insertions(+), 137 deletions(-) rename {charger => api}/decoder.go (73%) delete mode 100644 core/soc.go delete mode 100755 modules/audi/audi.py delete mode 100755 modules/audi/audi.sh create mode 100644 vehicle/audi.go create mode 100644 vehicle/config.go create mode 100644 vehicle/tesla.go create mode 100644 vehicle/vehicle.go diff --git a/Dockerfile b/Dockerfile index 0725f6fc3..efbf6bd85 100644 --- a/Dockerfile +++ b/Dockerfile @@ -27,15 +27,11 @@ FROM alpine:3.11 WORKDIR /evcc -# Audi -RUN apk add --no-cache python3 curl jq -RUN pip3 install --no-cache-dir --upgrade requests audiapi - -# Import from builder. +# Import from builder +COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ COPY --from=builder /build/evcc /usr/local/bin/evcc -COPY modules/audi/* /evcc/ COPY entrypoint.sh /evcc/ EXPOSE 7070 diff --git a/README.md b/README.md index a763f6f1a..2e40fa4f4 100644 --- a/README.md +++ b/README.md @@ -143,13 +143,21 @@ Meters provide data about power and energy consumption: Meter has a single implementation where meter readings- power and energy- can be configured to be delivered by plugin. -### SoC +### Vehicle -SoC represents a specific EV battery. Configuring a SoC allows to define it's `Capacity (kWh)` and dynamically provide: +Vehicle represents a specific EV vehicle and its battery: +- `Title()`: vehicle name for display in the configuration UI +- `Capacity()`: battery capacity in kWh - `ChargeState()`: state of charge in % -If SoC is configured and assigned to the charger, charge status and remaining charge duration become available in the user interface. +Optionally, a vehicle can optionally also provide: + +- `CurrentPower()`: charge power in W +- `ChargedEnergy()`: charged energy in kWh +- `ChargeDuration()`: charge duration + +If vehicle is configured and assigned to the charger, charge status and remaining charge duration become available in the user interface. ### Loadpoint diff --git a/api/api.go b/api/api.go index 6cb2f16f0..3fa90b068 100644 --- a/api/api.go +++ b/api/api.go @@ -55,7 +55,9 @@ type ChargeRater interface { ChargedEnergy() (float64, error) } -// SoC represents the EV battery's state -type SoC interface { +// Vehicle represents the EV and it's battery +type Vehicle interface { + Title() string + Capacity() int64 ChargeState() (float64, error) } diff --git a/charger/decoder.go b/api/decoder.go similarity index 73% rename from charger/decoder.go rename to api/decoder.go index 3b4c3c3a2..4ad39391c 100644 --- a/charger/decoder.go +++ b/api/decoder.go @@ -1,11 +1,11 @@ -package charger +package api import ( - "github.com/andig/evcc/api" "github.com/mitchellh/mapstructure" ) -func decodeOther(log *api.Logger, other map[string]interface{}, cc interface{}) { +// DecodeOther decodes string map into target configuration +func DecodeOther(log *Logger, other map[string]interface{}, cc interface{}) { decoderConfig := &mapstructure.DecoderConfig{ Result: cc, ErrorUnused: true, diff --git a/charger/charger.go b/charger/charger.go index 72afb9a0b..f92004ed1 100644 --- a/charger/charger.go +++ b/charger/charger.go @@ -16,7 +16,7 @@ type Charger struct { // NewConfigurableFromConfig creates a new configurable charger func NewConfigurableFromConfig(log *api.Logger, other map[string]interface{}) api.Charger { cc := struct{ Status, Enable, Enabled, MaxCurrent *provider.Config }{} - decodeOther(log, other, &cc) + api.DecodeOther(log, other, &cc) charger := NewConfigurable( provider.NewStringGetterFromConfig(cc.Status), diff --git a/charger/go-e.go b/charger/go-e.go index abedcfc5a..26509d3b8 100644 --- a/charger/go-e.go +++ b/charger/go-e.go @@ -33,7 +33,7 @@ type GoE struct { // NewGoEFromConfig creates a go-e charger from generic config func NewGoEFromConfig(log *api.Logger, other map[string]interface{}) api.Charger { cc := struct{ URI string }{} - decodeOther(log, other, &cc) + api.DecodeOther(log, other, &cc) return NewGoE(cc.URI) } diff --git a/charger/nrgkick.go b/charger/nrgkick.go index b2671d1b9..155046c91 100644 --- a/charger/nrgkick.go +++ b/charger/nrgkick.go @@ -68,7 +68,7 @@ type NRGKick struct { // NewNRGKickFromConfig creates a NRGKick charger from generic config func NewNRGKickFromConfig(log *api.Logger, other map[string]interface{}) api.Charger { cc := struct{ IP, MacAddress, Password string }{} - decodeOther(log, other, &cc) + api.DecodeOther(log, other, &cc) return NewNRGKick(cc.IP, cc.MacAddress, cc.Password) } diff --git a/charger/phoenix.go b/charger/phoenix.go index 59855bc87..304399dab 100644 --- a/charger/phoenix.go +++ b/charger/phoenix.go @@ -26,7 +26,7 @@ type Phoenix struct { // NewPhoenixFromConfig creates a Phoenix charger from generic config func NewPhoenixFromConfig(log *api.Logger, other map[string]interface{}) api.Charger { cc := struct{ URI string }{} - decodeOther(log, other, &cc) + api.DecodeOther(log, other, &cc) return NewPhoenix(cc.URI) } diff --git a/charger/simpleevse.go b/charger/simpleevse.go index 28600cf74..de83fe7b3 100644 --- a/charger/simpleevse.go +++ b/charger/simpleevse.go @@ -24,7 +24,7 @@ const ( // NewSimpleEVSEFromConfig creates a SimpleEVSE charger from generic config func NewSimpleEVSEFromConfig(log *api.Logger, other map[string]interface{}) api.Charger { cc := struct{ URI, Device string }{} - decodeOther(log, other, &cc) + api.DecodeOther(log, other, &cc) return NewSimpleEVSE(cc.URI, cc.Device) } diff --git a/charger/wallbe.go b/charger/wallbe.go index a04effbed..e96b5d419 100644 --- a/charger/wallbe.go +++ b/charger/wallbe.go @@ -35,7 +35,7 @@ type Wallbe struct { // NewWallbeFromConfig creates a Wallbe charger from generic config func NewWallbeFromConfig(log *api.Logger, other map[string]interface{}) api.Charger { cc := struct{ URI string }{} - decodeOther(log, other, &cc) + api.DecodeOther(log, other, &cc) return NewWallbe(cc.URI) } diff --git a/cmd/config.go b/cmd/config.go index 56b5ea9dd..c3441b742 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -18,11 +18,16 @@ type config struct { Menu []server.MenuConfig Pushover messagingConfig Meters []meterConfig - Chargers []chargerConfig - SoCs []socConfig + Chargers []typedConfig + Vehicles []typedConfig LoadPoints []loadPointConfig } +type typedConfig struct { + Name, Type string + Other map[string]interface{} `mapstructure:",remain"` +} + type messagingConfig struct { App string Recipients []string @@ -50,27 +55,13 @@ type meterConfig struct { Energy *provider.Config } -type chargerConfig struct { - Name string - Type string - Other map[string]interface{} `mapstructure:",remain"` -} - -type socConfig struct { - Name string - Type string - Title string - Capacity int64 - Charge *provider.Config -} - type loadPointConfig struct { Name string GridMeter string // api.Meter PVMeter string // api.Meter ChargeMeter string // api.Meter Charger string // api.Charger - SoC string // api.SoC + Vehicle string // api.Vehicle Mode api.ChargeMode Phases int64 MinCurrent int64 diff --git a/cmd/setup.go b/cmd/setup.go index dfeacc57b..c4cabbc68 100644 --- a/cmd/setup.go +++ b/cmd/setup.go @@ -11,6 +11,7 @@ import ( "github.com/andig/evcc/core/wrapper" "github.com/andig/evcc/provider" "github.com/andig/evcc/push" + "github.com/andig/evcc/vehicle" "github.com/spf13/viper" ) @@ -48,11 +49,10 @@ func configureChargers(conf config) (chargers map[string]api.Charger) { return } -func configureSoCs(conf config) (socs map[string]api.SoC) { - socs = make(map[string]api.SoC) - for _, sc := range conf.SoCs { - soc := core.NewSoC(sc.Capacity, sc.Title, provider.NewFloatGetterFromConfig(sc.Charge)) - socs[sc.Name] = soc +func configureVehicles(conf config) (vehicles map[string]api.Vehicle) { + vehicles = make(map[string]api.Vehicle) + for _, cc := range conf.Vehicles { + vehicles[cc.Name] = vehicle.NewFromConfig(log, cc.Type, cc.Other) } return } @@ -97,7 +97,7 @@ func loadConfig(conf config, eventsChan chan push.Event) (loadPoints []*core.Loa meters := configureMeters(conf) chargers := configureChargers(conf) - socs := configureSoCs(conf) + vehicles := configureVehicles(conf) for idx, lpc := range conf.LoadPoints { // configure loadpoint @@ -131,11 +131,11 @@ func loadConfig(conf config, eventsChan chan push.Event) (loadPoints []*core.Loa } // assign socs - if lpc.SoC != "" { - if impl, ok := socs[lpc.SoC]; ok { - lp.SoC = impl + if lpc.Vehicle != "" { + if impl, ok := vehicles[lpc.Vehicle]; ok { + lp.Vehicle = impl } else { - log.FATAL.Fatalf("invalid soc '%s'", lpc.SoC) + log.FATAL.Fatalf("invalid vehicle '%s'", lpc.Vehicle) } } diff --git a/core/api.go b/core/api.go index b6b169b65..68e89be90 100644 --- a/core/api.go +++ b/core/api.go @@ -40,12 +40,10 @@ func (lp *LoadPoint) Configuration() Configuration { ChargeMeter: lp.hasChargeMeter(), } - if lp.SoC != nil { + if lp.Vehicle != nil { c.SoC = true - if soc, ok := lp.SoC.(*SoC); ok { - c.SoCCapacity = soc.Capacity - c.SoCTitle = soc.Title - } + c.SoCCapacity = lp.Vehicle.Capacity() + c.SoCTitle = lp.Vehicle.Title() } return c @@ -58,11 +56,11 @@ func (lp *LoadPoint) hasChargeMeter() bool { // Dump loadpoint configuration func (lp *LoadPoint) Dump() { - soc := lp.SoC != nil + vehicle := lp.Vehicle != nil grid := lp.GridMeter != nil pv := lp.PVMeter != nil - log.INFO.Printf("%s config: soc %s grid %s pv %s charge %s", lp.Name, - presence[soc], + log.INFO.Printf("%s config: vehicle %s grid %s pv %s charge %s", lp.Name, + presence[vehicle], presence[grid], presence[pv], presence[lp.hasChargeMeter()], @@ -121,11 +119,9 @@ func (lp *LoadPoint) remainingChargeDuration(chargePercent float64) time.Duratio return -1 } - if lp.chargePower > 0 { - if soc, ok := lp.SoC.(*SoC); ok { - whRemaining := (1 - chargePercent/100.0) * 1e3 * float64(soc.Capacity) - return time.Duration(float64(time.Hour) * whRemaining / lp.chargePower) - } + if lp.chargePower > 0 && lp.Vehicle != nil { + whRemaining := (1 - chargePercent/100.0) * 1e3 * float64(lp.Vehicle.Capacity()) + return time.Duration(float64(time.Hour) * whRemaining / lp.chargePower) } return -1 @@ -133,19 +129,19 @@ func (lp *LoadPoint) remainingChargeDuration(chargePercent float64) time.Duratio // publish state of charge and remaining charge duration func (lp *LoadPoint) publishSoC() { - if lp.SoC == nil { + if lp.Vehicle == nil { return } if lp.connected() { - f, err := lp.SoC.ChargeState() + f, err := lp.Vehicle.ChargeState() if err == nil { - log.DEBUG.Printf("%s soc charge: %.1f%%", lp.Name, f) + log.DEBUG.Printf("%s vehicle charge: %.1f%%", lp.Name, f) lp.publish("socCharge", f) lp.publish("chargeEstimate", lp.remainingChargeDuration(f)) return } - log.ERROR.Printf("%s soc error: %v", lp.Name, err) + log.ERROR.Printf("%s vehicle error: %v", lp.Name, err) } lp.publish("socCharge", "—") diff --git a/core/loadpoint.go b/core/loadpoint.go index d94639d74..8c05dc98c 100644 --- a/core/loadpoint.go +++ b/core/loadpoint.go @@ -46,14 +46,14 @@ type LoadPoint struct { ChargeRater api.ChargeRater // meters - GridMeter api.Meter // Grid usage meter - PVMeter api.Meter // PV generation meter - ChargeMeter api.Meter // Charger usage meter - SoC api.SoC // SoC + GridMeter api.Meter // Grid usage meter + PVMeter api.Meter // PV generation meter + ChargeMeter api.Meter // Charger usage meter + Vehicle api.Vehicle // Vehicle // options Steepness int64 // Step size of current change - Phases int64 // SOC phases. Required for converting power and current. + Phases int64 // Phases- required for converting power and current. MinCurrent int64 // PV mode: start current Min+PV mode: min current MaxCurrent int64 // Max allowed current. Physically ensured by the charge controller Voltage float64 // Operating voltage. 230V for Germany. diff --git a/core/soc.go b/core/soc.go deleted file mode 100644 index cb3d6aef4..000000000 --- a/core/soc.go +++ /dev/null @@ -1,27 +0,0 @@ -package core - -import ( - "github.com/andig/evcc/api" - "github.com/andig/evcc/provider" -) - -// SoC is an api.SoC implementation with configurable getters and setters. -type SoC struct { - Capacity int64 - Title string - chargeG provider.FloatGetter -} - -// NewSoC creates a new SoC -func NewSoC(capacity int64, title string, chargeG provider.FloatGetter) api.SoC { - return &SoC{ - Capacity: capacity, - Title: title, - chargeG: chargeG, - } -} - -// ChargeState implements the SoC.ChargeState interface -func (m *SoC) ChargeState() (float64, error) { - return m.chargeG() -} diff --git a/go.mod b/go.mod index d14a6856f..8989e8279 100644 --- a/go.mod +++ b/go.mod @@ -18,6 +18,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/jsgoecke/tesla v0.0.0-20190206234002-112508e1374e github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 github.com/mitchellh/mapstructure v1.2.2 github.com/mjibson/esc v0.2.0 diff --git a/go.sum b/go.sum index 85bb231f4..7687a3708 100644 --- a/go.sum +++ b/go.sum @@ -133,6 +133,8 @@ github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANyt 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/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= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= diff --git a/modules/audi/audi.py b/modules/audi/audi.py deleted file mode 100755 index 8cd2ff36c..000000000 --- a/modules/audi/audi.py +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env python3 -import json, os - -from audiapi.API import API -from audiapi.Services import LogonService - -credentials = 'audi.json' - -if __name__ == '__main__': - api = API() - logon_service = LogonService(api) - if not logon_service.restore_token(): - # We need to login - if os.path.isfile(credentials): - with open(credentials) as data_file: - data = json.load(data_file) - logon_service.login(data['user'], data['pass']) - else: - user = os.environ['AUDI_USER'] - password = os.environ['AUDI_PASS'] - logon_service.login(user, password) diff --git a/modules/audi/audi.sh b/modules/audi/audi.sh deleted file mode 100755 index ecb1625e8..000000000 --- a/modules/audi/audi.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/sh - -set -e - -AUDI_USER=$(cat audi.json | jq -r .user) -AUDI_PASS=$(cat audi.json | jq -r .pass) -python3 audi.py - -acctoken=$(cat token.json | jq -r .access_token) -vin=$(cat audi.json | jq -r .vin) -battsoc=$(curl -s --header "Accept: application/json" --header "X-App-Name: eRemote" --header "X-App-Version: 1.0.0" --header "User-Agent: okhttp/2.3.0" --header "Authorization: AudiAuth 1 $acctoken" https://msg.audi.de/fs-car/bs/batterycharge/v1/Audi/DE/vehicles/$vin/charger) -soclevel=$(echo $battsoc | jq .charger.status.batteryStatusData.stateOfCharge.content) -echo $soclevel diff --git a/tmpl.Dockerfile b/tmpl.Dockerfile index 70d6e0396..19ccaeff5 100644 --- a/tmpl.Dockerfile +++ b/tmpl.Dockerfile @@ -24,16 +24,14 @@ RUN GOARCH={{ .GoARCH }} GOARM={{ .GoARM }} make build ############################# FROM {{ .RuntimeImage }} -# Import from builder. +# Import from builder COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ -COPY --from=builder /etc/passwd /etc/passwd - -# Copy our static executable. COPY --from=builder /build/evcc /usr/local/bin/evcc -# UI and API port +COPY entrypoint.sh /evcc/ + EXPOSE 7070 -# Run the binary -ENTRYPOINT ["/usr/local/bin/evcc"] +ENTRYPOINT [ "/evcc/entrypoint.sh" ] +CMD [ "evcc" ] diff --git a/vehicle/audi.go b/vehicle/audi.go new file mode 100644 index 000000000..c47b66d30 --- /dev/null +++ b/vehicle/audi.go @@ -0,0 +1,208 @@ +package vehicle + +import ( + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "net/http" + "net/url" + "strings" + "time" + + "github.com/andig/evcc/api" + "github.com/andig/evcc/provider" +) + +const ( + audiURL = "https://msg.audi.de/fs-car" + audiDE = "Audi/DE" + audiAuthPrefix = "AudiAuth 1" + audiValidMargin = 10 * time.Second +) + +// Audi is an api.Vehicle implementation for Audi cars +type Audi struct { + *embed + user, password, vin string + token string + tokenValid time.Time + cache time.Duration + chargeStateVal float64 + chargeStateUpdated time.Time + chargeStateG *provider.CacheGetter +} + +type audiTokenResponse struct { + AccessToken string `json:"access_token"` + TokenType string `json:"token_type"` + ExpiresIn int `json:"expires_in"` +} +type audiErrorResponse struct { + Error string + Description string `json:"error_description"` +} + +type audiBatteryResponse struct { + Charger audiBrCharger +} + +type audiBrCharger struct { + Status audiBrStatus +} + +type audiBrStatus struct { + BatteryStatusData audiBrStatusData +} + +type audiBrStatusData struct { + StateOfCharge audiBrStateOfCharge +} + +type audiBrStateOfCharge struct { + Content int +} + +// NewAudiFromConfig creates a new vehicle +func NewAudiFromConfig(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 := &Audi{ + embed: &embed{cc.Title, cc.Capacity}, + user: cc.User, + password: cc.Password, + vin: cc.VIN, + cache: cc.Cache, + } + + v.chargeStateG = provider.NewCacheGetter(v.chargeState, cc.Cache) + + return v +} + +func (v *Audi) apiURL(service, part string) string { + return fmt.Sprintf("%s/%s/%s/%s/%s", audiURL, service, "v1", audiDE, part) +} + +func (v *Audi) headers(header *http.Header) { + for k, v := range map[string]string{ + "Accept": "application/json", + "X-App-ID": "de.audi.mmiapp", + "X-App-Name": "MMIconnect", + "X-App-Version": "2.8.3", + "X-Brand": "audi", + "X-Country-Id": "DE", + "X-Language-Id": "de", + "X-Platform": "google", + "User-Agent": "okhttp/2.7.4", + "ADRUM_1": "isModule:true", + "ADRUM": "isAray:true", + } { + header.Set(k, v) + } +} + +func (v *Audi) login(user, password string) error { + uri := v.apiURL("core/auth", "token") + + data := url.Values{ + "grant_type": []string{"password"}, + "username": []string{user}, + "password": []string{password}, + } + + req, err := http.NewRequest(http.MethodPost, uri, strings.NewReader(data.Encode())) + if err != nil { + return err + } + + v.headers(&req.Header) + req.Header.Set("Authorization", audiAuthPrefix) + 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 { + var er audiErrorResponse + if err = json.Unmarshal(b, &er); err == nil { + return errors.New(er.Description) + } + return fmt.Errorf("unexpected response %d: %s", resp.StatusCode, string(b)) + } + + var tr audiTokenResponse + if err = json.Unmarshal(b, &tr); err != nil { + return err + } + + v.token = tr.AccessToken + v.tokenValid = time.Now().Add(time.Duration(tr.ExpiresIn)*time.Second - audiValidMargin) + + return nil +} + +func (v *Audi) 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 + } + + v.headers(&req.Header) + req.Header.Set("Authorization", fmt.Sprintf("%s %s", audiAuthPrefix, v.token)) + + return req, nil +} + +// chargeState implements the Vehicle.ChargeState interface +func (v *Audi) chargeState() (float64, error) { + uri := v.apiURL("bs/batterycharge", fmt.Sprintf("vehicles/%s/charger", 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 audiBatteryResponse + err = json.Unmarshal(b, &br) + + return float64(br.Charger.Status.BatteryStatusData.StateOfCharge.Content), err +} + +// ChargeState implements the Vehicle.ChargeState interface +func (v *Audi) ChargeState() (float64, error) { + return v.chargeStateG.FloatGetter() +} diff --git a/vehicle/config.go b/vehicle/config.go new file mode 100644 index 000000000..2b9c3b2ad --- /dev/null +++ b/vehicle/config.go @@ -0,0 +1,23 @@ +package vehicle + +import ( + "github.com/andig/evcc/api" +) + +// NewFromConfig creates charger from configuration +func NewFromConfig(log *api.Logger, typ string, other map[string]interface{}) api.Vehicle { + var c api.Vehicle + + switch typ { + case "script": + c = NewConfigurableFromConfig(log, other) + case "audi": + c = NewAudiFromConfig(log, other) + case "tesla": + c = NewTeslaFromConfig(log, other) + default: + log.FATAL.Fatalf("invalid vehicle type '%s'", typ) + } + + return c +} diff --git a/vehicle/tesla.go b/vehicle/tesla.go new file mode 100644 index 000000000..2b4cd69f7 --- /dev/null +++ b/vehicle/tesla.go @@ -0,0 +1,84 @@ +package vehicle + +import ( + "time" + + "github.com/andig/evcc/api" + "github.com/andig/evcc/provider" + "github.com/jsgoecke/tesla" +) + +// Tesla is an api.Vehicle implementation for Tesla cars +type Tesla struct { + *embed + vehicle *tesla.Vehicle + chargeStateG *provider.CacheGetter + chargedEnergyG *provider.CacheGetter +} + +// NewTeslaFromConfig creates a new Tesla vehicle +func NewTeslaFromConfig(log *api.Logger, other map[string]interface{}) api.Vehicle { + cc := struct { + Title string + Capacity int64 + ClientID, ClientSecret, Email, Password string + Vehicle int + Cache time.Duration + }{} + api.DecodeOther(log, other, &cc) + + client, err := tesla.NewClient(&tesla.Auth{ + ClientID: cc.ClientID, + ClientSecret: cc.ClientSecret, + Email: cc.Email, + Password: cc.Password, + }) + if err != nil { + log.FATAL.Fatalf("cannot create tesla: %v", err) + } + + vehicles, err := client.Vehicles() + if err != nil { + log.FATAL.Fatalf("cannot create tesla: %v", err) + } + + v := &Tesla{ + embed: &embed{cc.Title, cc.Capacity}, + vehicle: vehicles[0].Vehicle, + } + + v.chargeStateG = provider.NewCacheGetter(v.chargeState, cc.Cache) + v.chargedEnergyG = provider.NewCacheGetter(v.chargedEnergy, cc.Cache) + + return v +} + +// chargeState implements the Vehicle.ChargeState interface +func (v *Tesla) chargeState() (float64, error) { + state, err := v.vehicle.ChargeState() + return float64(state.BatteryLevel), err +} + +// ChargeState implements the Vehicle.ChargeState interface +func (v *Tesla) ChargeState() (float64, error) { + return v.chargeStateG.FloatGetter() +} + +// chargedEnergy implements the ChargeRater.ChargedEnergy interface +func (v *Tesla) chargedEnergy() (float64, error) { + state, err := v.vehicle.ChargeState() + return state.ChargeEnergyAdded, err +} + +// ChargedEnergy implements the ChargeRater.ChargedEnergy interface +func (v *Tesla) ChargedEnergy() (float64, error) { + return v.chargedEnergyG.FloatGetter() +} + +// depends on https://github.com/jsgoecke/tesla/issues/28 +// +// CurrentPower implements the ChargeRater.CurrentPower interface +// func (v *Tesla) CurrentPower() (float64, error) { +// state, err := v.vehicle.ChargeState() +// return state.ChargerPower, err +// } diff --git a/vehicle/vehicle.go b/vehicle/vehicle.go new file mode 100644 index 000000000..289fe3c9a --- /dev/null +++ b/vehicle/vehicle.go @@ -0,0 +1,47 @@ +package vehicle + +import ( + "github.com/andig/evcc/api" + "github.com/andig/evcc/provider" +) + +type embed struct { + title string + capacity int64 +} + +// Title implements the Vehicle.Title interface +func (m *embed) Title() string { + return m.title +} + +// Capacity implements the Vehicle.Capacity interface +func (m *embed) Capacity() int64 { + return m.capacity +} + +// Vehicle is an api.Vehicle implementation with configurable getters and setters. +type Vehicle struct { + *embed + chargeG provider.FloatGetter +} + +// NewConfigurableFromConfig creates a new Vehicle +func NewConfigurableFromConfig(log *api.Logger, other map[string]interface{}) api.Vehicle { + cc := struct { + Title string + Capacity int64 + Charge *provider.Config + }{} + api.DecodeOther(log, other, &cc) + + return &Vehicle{ + embed: &embed{cc.Title, cc.Capacity}, + chargeG: provider.NewFloatGetterFromConfig(cc.Charge), + } +} + +// ChargeState implements the Vehicle.ChargeState interface +func (m *Vehicle) ChargeState() (float64, error) { + return m.chargeG() +}