Skip to content

Commit

Permalink
Support go-e cloud api (#190)
Browse files Browse the repository at this point in the history
  • Loading branch information
andig committed Jun 18, 2020
1 parent fade1d5 commit 279dfe6
Show file tree
Hide file tree
Showing 4 changed files with 105 additions and 33 deletions.
2 changes: 1 addition & 1 deletion README.md
Expand Up @@ -102,7 +102,7 @@ Charger is responsible for handling EV state and adjusting charge current. Avail
- `evsewifi`: chargers with SimpleEVSE controllers using [EVSE-WiFi](https://www.evse-wifi.de/)
- `nrgkick-bt`: NRGkick chargers with Bluetooth connector (Linux only, not supported on Docker)
- `nrgkick-connect`: NRGkick chargers with additional NRGkick Connect module
- `go-e`: go-eCharger chargers
- `go-e`: go-eCharger chargers (both local and cloud API are supported)
- `keba`: KEBA KeContact P20/P30 and BMW chargers (see [Preparation](#keba-preparation))
- `mcc`: Mobile Charger Connect devices (Audi, Bentley, Porsche)
- `default`: default charger implementation using configurable [plugins](#plugins) for integrating any type of charger
Expand Down
2 changes: 1 addition & 1 deletion charger/go-e-test.go
Expand Up @@ -8,7 +8,7 @@ import (

// TestGoE tests interfaces
func TestGoE(t *testing.T) {
var wb api.Charger = NewGoE("foo")
var wb api.Charger = NewGoE("foo", "bar", 0)

if _, ok := wb.(api.MeterCurrent); !ok {
t.Error("missing MeterCurrent interface")
Expand Down
130 changes: 100 additions & 30 deletions charger/go-e.go
@@ -1,17 +1,26 @@
package charger

import (
"errors"
"fmt"
"strings"
"time"

"github.com/andig/evcc/api"
"github.com/andig/evcc/util"
)

const (
goeStatus apiFunction = "status"
goePayload apiFunction = "mqtt?payload="
)
// https://go-e.co/app/api.pdf

const goeCloud = "https://api.go-e.co"

// goeCloudResponse is the cloud API response
type goeCloudResponse struct {
Success *bool `json:"success"` // only valid for cloud payload commands
Age int `json:"age"`
Error string `json:"error"` // only valid for cloud payload commands
Data goeStatusResponse `json:"data"`
}

// goeStatusResponse is the API response if status not OK
type goeStatusResponse struct {
Expand All @@ -28,35 +37,106 @@ type goeStatusResponse struct {
// GoE charger implementation
type GoE struct {
*util.HTTPHelper
uri string
uri, token string
cache time.Duration
updated time.Time
status goeStatusResponse
}

// NewGoEFromConfig creates a go-e charger from generic config
func NewGoEFromConfig(log *util.Logger, other map[string]interface{}) api.Charger {
cc := struct{ URI string }{}
cc := struct {
Token string
URI string
Cache time.Duration
}{}
util.DecodeOther(log, other, &cc)

return NewGoE(cc.URI)
if cc.URI != "" && cc.Token != "" {
log.FATAL.Fatal("config: should only have one of uri/token")
}
if cc.URI == "" && cc.Token == "" {
log.FATAL.Fatal("config: must have one of uri/token")
}

return NewGoE(cc.URI, cc.Token, cc.Cache)
}

// NewGoE creates GoE charger
func NewGoE(URI string) *GoE {
func NewGoE(uri, token string, cache time.Duration) *GoE {
c := &GoE{
HTTPHelper: util.NewHTTPHelper(util.NewLogger("go-e")),
uri: strings.TrimRight(URI, "/"),
uri: strings.TrimRight(uri, "/"),
token: strings.TrimSpace(token),
}

return c
}

func (c *GoE) apiURL(api apiFunction) string {
return fmt.Sprintf("%s/%s", c.uri, api)
func (c *GoE) localResponse(function, payload string) (goeStatusResponse, error) {
var status goeStatusResponse

url := fmt.Sprintf("%s/%s", c.uri, function)
if payload != "" {
url += "&payload=" + payload
}

_, err := c.GetJSON(url, &status)
return status, err
}

func (c *GoE) cloudResponse(function, payload string) (goeStatusResponse, error) {
var status goeCloudResponse

url := fmt.Sprintf("%s/%s?token=%s", goeCloud, function, c.token)
if payload != "" {
url += "&payload=" + payload
}

_, err := c.GetJSON(url, &status)
if err == nil && status.Success != nil && !*status.Success {
err = errors.New(status.Error)
}

return status.Data, err
}

func (c *GoE) apiStatus() (status goeStatusResponse, err error) {
if c.token == "" {
return c.localResponse("status", "")
}

status = c.status // cached value

if time.Since(c.updated) >= c.cache {
status, err = c.cloudResponse("api_status", "")
if err == nil {
c.updated = time.Now()
c.status = status
}
}

return status, err
}

func (c *GoE) apiUpdate(payload string) (goeStatusResponse, error) {
if c.token == "" {
return c.localResponse("mqtt", payload)
}

status, err := c.cloudResponse("api", payload)
if err == nil {
c.updated = time.Now()
c.status = status
}

return status, err
}

// Status implements the Charger.Status interface
func (c *GoE) Status() (api.ChargeStatus, error) {
var status goeStatusResponse
if _, err := c.GetJSON(c.apiURL(goeStatus), &status); err != nil {
status, err := c.apiStatus()
if err != nil {
return api.StatusNone, err
}

Expand All @@ -74,8 +154,8 @@ func (c *GoE) Status() (api.ChargeStatus, error) {

// Enabled implements the Charger.Enabled interface
func (c *GoE) Enabled() (bool, error) {
var status goeStatusResponse
if _, err := c.GetJSON(c.apiURL(goeStatus), &status); err != nil {
status, err := c.apiStatus()
if err != nil {
return false, err
}

Expand All @@ -91,16 +171,12 @@ func (c *GoE) Enabled() (bool, error) {

// Enable implements the Charger.Enable interface
func (c *GoE) Enable(enable bool) error {
var status goeStatusResponse

var b int
if enable {
b = 1
}

uri := c.apiURL(goePayload) + fmt.Sprintf("alw=%d", b)

_, err := c.GetJSON(uri, &status)
status, err := c.apiUpdate(fmt.Sprintf("alw=%d", b))
if err == nil && status.Alw != b {
return fmt.Errorf("alw update failed: %d", status.Amp)
}
Expand All @@ -110,10 +186,7 @@ func (c *GoE) Enable(enable bool) error {

// MaxCurrent implements the Charger.MaxCurrent interface
func (c *GoE) MaxCurrent(current int64) error {
var status goeStatusResponse
uri := c.apiURL(goePayload) + fmt.Sprintf("amp=%d", current)

_, err := c.GetJSON(uri, &status)
status, err := c.apiUpdate(fmt.Sprintf("amp=%d", current))
if err == nil && int64(status.Amp) != current {
return fmt.Errorf("amp update failed: %d", status.Amp)
}
Expand All @@ -123,8 +196,7 @@ func (c *GoE) MaxCurrent(current int64) error {

// CurrentPower implements the Meter interface.
func (c *GoE) CurrentPower() (float64, error) {
var status goeStatusResponse
_, err := c.GetJSON(c.apiURL(goeStatus), &status)
status, err := c.apiStatus()
var power float64
if len(status.Nrg) == 16 {
power = float64(status.Nrg[11]) * 10
Expand All @@ -134,16 +206,14 @@ func (c *GoE) CurrentPower() (float64, error) {

// ChargedEnergy implements the ChargeRater interface
func (c *GoE) ChargedEnergy() (float64, error) {
var status goeStatusResponse
_, err := c.GetJSON(c.apiURL(goeStatus), &status)
status, err := c.apiStatus()
energy := float64(status.Dws) / 3.6e5 // Deka-Watt-Seconds to kWh (100.000 == 0,277kWh)
return energy, err
}

// Currents implements the MeterCurrent interface
func (c *GoE) Currents() (float64, float64, float64, error) {
var status goeStatusResponse
_, err := c.GetJSON(c.apiURL(goeStatus), &status)
status, err := c.apiStatus()
if len(status.Nrg) == 16 {
return float64(status.Nrg[4]) / 10, float64(status.Nrg[5]) / 10, float64(status.Nrg[6]) / 10, nil
}
Expand Down
4 changes: 3 additions & 1 deletion evcc.dist.yaml
Expand Up @@ -120,7 +120,9 @@ chargers:
# pin: # pin
- name: go-e
type: go-e # go-eCharger
uri: http://192.168.1.4 # go-e address
uri: http://192.168.1.4 # either go-e local address
token: 4711c # or go-e cloud API token
cache: 10s # go-e cloud API cache duration
- name: keba
type: keba # KEBA charger
uri: 192.168.1.4:7090 # KEBA address
Expand Down

0 comments on commit 279dfe6

Please sign in to comment.