Support Audi and Tesla vehicle APIs (#20)

This commit is contained in:
andig 2020-03-29 21:15:44 +02:00 • committed by GitHub
parent 0a63ceb90a
commit ef6140cce5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 432 additions and 137 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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", "—")

View file

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

View file

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

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

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

View file

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

View file

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

View file

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