Support Audi and Tesla vehicle APIs (#20)
This commit is contained in:
parent
0a63ceb90a
commit
ef6140cce5
24 changed files with 432 additions and 137 deletions
|
|
@ -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
|
||||
|
|
|
|||
14
README.md
14
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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
20
cmd/setup.go
20
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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
30
core/api.go
30
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", "—")
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
27
core/soc.go
27
core/soc.go
|
|
@ -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()
|
||||
}
|
||||
1
go.mod
1
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
|
||||
|
|
|
|||
2
go.sum
2
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=
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
|
|
@ -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" ]
|
||||
|
|
|
|||
208
vehicle/audi.go
Normal file
208
vehicle/audi.go
Normal file
|
|
@ -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()
|
||||
}
|
||||
23
vehicle/config.go
Normal file
23
vehicle/config.go
Normal file
|
|
@ -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
|
||||
}
|
||||
84
vehicle/tesla.go
Normal file
84
vehicle/tesla.go
Normal file
|
|
@ -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
|
||||
// }
|
||||
47
vehicle/vehicle.go
Normal file
47
vehicle/vehicle.go
Normal file
|
|
@ -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()
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue