Add Go-E v2 api support (#1609)
This commit is contained in:
parent
71f0cae5ea
commit
a90c69e8b3
7 changed files with 625 additions and 165 deletions
253
charger/go-e.go
253
charger/go-e.go
|
|
@ -3,90 +3,29 @@ package charger
|
|||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/evcc-io/evcc/api"
|
||||
goe "github.com/evcc-io/evcc/charger/go-e"
|
||||
"github.com/evcc-io/evcc/util"
|
||||
"github.com/evcc-io/evcc/util/request"
|
||||
"github.com/evcc-io/evcc/util/sponsor"
|
||||
)
|
||||
|
||||
// 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 {
|
||||
Fwv string `json:"fwv"` // firmware version - indicates local response
|
||||
Car int `json:"car,string"` // car status
|
||||
Alw int `json:"alw,string"` // allow charging
|
||||
Amp int `json:"amp,string"` // current [A]
|
||||
Err int `json:"err,string"` // error
|
||||
Stp int `json:"stp,string"` // stop state
|
||||
Tmp int `json:"tmp,string"` // temperature [°C]
|
||||
Dws int `json:"dws,string"` // energy [Ws]
|
||||
Nrg []int `json:"nrg"` // voltage, current, power
|
||||
Uby int `json:"uby,string"` // unlocked_by
|
||||
Rna string `json:"rna"` // RFID 1
|
||||
Rnm string `json:"rnm"` // RFID 2
|
||||
Rne string `json:"rne"` // RFID 3
|
||||
Rn4 string `json:"rn4"` // RFID 4
|
||||
Rn5 string `json:"rn5"` // RFID 5
|
||||
Rn6 string `json:"rn6"` // RFID 6
|
||||
Rn7 string `json:"rn7"` // RFID 7
|
||||
Rn8 string `json:"rn8"` // RFID 8
|
||||
Rn9 string `json:"rn9"` // RFID 9
|
||||
Rn1 string `json:"rn1"` // RFID 10
|
||||
}
|
||||
|
||||
func (g *goeStatusResponse) RFIDName() string {
|
||||
switch g.Uby {
|
||||
case 1:
|
||||
return g.Rna
|
||||
case 2:
|
||||
return g.Rnm
|
||||
case 3:
|
||||
return g.Rne
|
||||
case 4:
|
||||
return g.Rn4
|
||||
case 5:
|
||||
return g.Rn5
|
||||
case 6:
|
||||
return g.Rn6
|
||||
case 7:
|
||||
return g.Rn7
|
||||
case 8:
|
||||
return g.Rn8
|
||||
case 9:
|
||||
return g.Rn9
|
||||
case 10:
|
||||
return g.Rn1
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
// https://github.com/goecharger/go-eCharger-API-v1/
|
||||
// https://github.com/goecharger/go-eCharger-API-v2/
|
||||
|
||||
// GoE charger implementation
|
||||
type GoE struct {
|
||||
*request.Helper
|
||||
uri, token string
|
||||
cache time.Duration
|
||||
updated time.Time
|
||||
status goeStatusResponse
|
||||
api goe.API
|
||||
}
|
||||
|
||||
func init() {
|
||||
registry.Add("go-e", NewGoEFromConfig)
|
||||
}
|
||||
|
||||
// go:generate go run ../cmd/tools/decorate.go -f decorateGoE -b *GoE -r api.Charger -t "api.MeterEnergy,TotalEnergy,func() (float64, error)" -t "api.ChargePhases,Phases1p3p,func(int) (error)"
|
||||
|
||||
// NewGoEFromConfig creates a go-e charger from generic config
|
||||
func NewGoEFromConfig(other map[string]interface{}) (api.Charger, error) {
|
||||
cc := struct {
|
||||
|
|
@ -94,99 +33,55 @@ func NewGoEFromConfig(other map[string]interface{}) (api.Charger, error) {
|
|||
URI string
|
||||
Cache time.Duration
|
||||
}{}
|
||||
|
||||
if err := util.DecodeOther(other, &cc); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if cc.URI != "" && cc.Token != "" {
|
||||
return nil, errors.New("go-e config: should only have one of uri/token")
|
||||
return nil, errors.New("should only have one of uri/token")
|
||||
}
|
||||
if cc.URI == "" && cc.Token == "" {
|
||||
return nil, errors.New("go-e config: must have one of uri/token")
|
||||
return nil, errors.New("must have one of uri/token")
|
||||
}
|
||||
|
||||
return NewGoE(cc.URI, cc.Token, cc.Cache)
|
||||
}
|
||||
|
||||
// NewGoE creates GoE charger
|
||||
func NewGoE(uri, token string, cache time.Duration) (*GoE, error) {
|
||||
c := &GoE{
|
||||
Helper: request.NewHelper(util.NewLogger("go-e")),
|
||||
uri: strings.TrimRight(uri, "/"),
|
||||
token: strings.TrimSpace(token),
|
||||
func NewGoE(uri, token string, cache time.Duration) (api.Charger, error) {
|
||||
c := &GoE{}
|
||||
|
||||
log := util.NewLogger("go-e")
|
||||
|
||||
if token != "" {
|
||||
c.api = goe.NewCloud(log, token, cache)
|
||||
} else {
|
||||
c.api = goe.NewLocal(log, uri)
|
||||
}
|
||||
|
||||
if c.api.IsV2() {
|
||||
var phases func(int) error
|
||||
if sponsor.IsAuthorized() {
|
||||
phases = c.phases1p3p
|
||||
} else {
|
||||
log.WARN.Println("automatic 1p3p phase switching requires sponsor token")
|
||||
}
|
||||
|
||||
return decorateGoE(c, c.totalEnergy, phases), nil
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
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", "")
|
||||
}
|
||||
|
||||
if time.Since(c.updated) >= c.cache {
|
||||
status, err = c.cloudResponse("api_status", "")
|
||||
if err == nil {
|
||||
c.updated = time.Now()
|
||||
c.status = status
|
||||
}
|
||||
}
|
||||
|
||||
return c.status, err
|
||||
}
|
||||
|
||||
// apiUpdate invokes either cloud or local api
|
||||
// goeStatusResponse is only valid for local api. Use Fwv if valid.
|
||||
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 {
|
||||
// cloud api sends partial response, don't store it as status
|
||||
c.updated = time.Time{}
|
||||
}
|
||||
|
||||
return status, err
|
||||
}
|
||||
|
||||
// Status implements the api.Charger interface
|
||||
func (c *GoE) Status() (api.ChargeStatus, error) {
|
||||
status, err := c.apiStatus()
|
||||
resp, err := c.api.Status()
|
||||
if err != nil {
|
||||
return api.StatusNone, err
|
||||
}
|
||||
|
||||
switch status.Car {
|
||||
switch car := resp.Status(); car {
|
||||
case 1:
|
||||
return api.StatusA, nil
|
||||
case 2:
|
||||
|
|
@ -194,25 +89,18 @@ func (c *GoE) Status() (api.ChargeStatus, error) {
|
|||
case 3, 4:
|
||||
return api.StatusB, nil
|
||||
default:
|
||||
return api.StatusNone, fmt.Errorf("car unknown result: %d", status.Car)
|
||||
return api.StatusNone, fmt.Errorf("car unknown result: %d", car)
|
||||
}
|
||||
}
|
||||
|
||||
// Enabled implements the api.Charger interface
|
||||
func (c *GoE) Enabled() (bool, error) {
|
||||
status, err := c.apiStatus()
|
||||
resp, err := c.api.Status()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
switch status.Alw {
|
||||
case 0:
|
||||
return false, nil
|
||||
case 1:
|
||||
return true, nil
|
||||
default:
|
||||
return false, fmt.Errorf("alw unknown result: %d", status.Alw)
|
||||
}
|
||||
return resp.Enabled(), nil
|
||||
}
|
||||
|
||||
// Enable implements the api.Charger interface
|
||||
|
|
@ -221,52 +109,87 @@ func (c *GoE) Enable(enable bool) error {
|
|||
if enable {
|
||||
b = 1
|
||||
}
|
||||
_, err := c.apiUpdate(fmt.Sprintf("alw=%d", b))
|
||||
return err
|
||||
|
||||
param := map[bool]string{false: "alw", true: "frc"}[c.api.IsV2()]
|
||||
if c.api.IsV2() {
|
||||
b += 1
|
||||
}
|
||||
|
||||
return c.api.Update(fmt.Sprintf("%s=%d", param, b))
|
||||
}
|
||||
|
||||
// MaxCurrent implements the api.Charger interface
|
||||
func (c *GoE) MaxCurrent(current int64) error {
|
||||
_, err := c.apiUpdate(fmt.Sprintf("amx=%d", current))
|
||||
return err
|
||||
param := map[bool]string{false: "amx", true: "amp"}[c.api.IsV2()]
|
||||
return c.api.Update(fmt.Sprintf("%s=%d", param, current))
|
||||
}
|
||||
|
||||
var _ api.Meter = (*GoE)(nil)
|
||||
|
||||
// CurrentPower implements the api.Meter interface
|
||||
func (c *GoE) CurrentPower() (float64, error) {
|
||||
status, err := c.apiStatus()
|
||||
var power float64
|
||||
if len(status.Nrg) == 16 {
|
||||
power = float64(status.Nrg[11]) * 10
|
||||
resp, err := c.api.Status()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return power, err
|
||||
|
||||
return resp.CurrentPower(), err
|
||||
}
|
||||
|
||||
var _ api.ChargeRater = (*GoE)(nil)
|
||||
|
||||
// ChargedEnergy implements the api.ChargeRater interface
|
||||
func (c *GoE) ChargedEnergy() (float64, error) {
|
||||
status, err := c.apiStatus()
|
||||
energy := float64(status.Dws) / 3.6e5 // Deka-Watt-Seconds to kWh (100.000 == 0,277kWh)
|
||||
return energy, err
|
||||
resp, err := c.api.Status()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return resp.ChargedEnergy(), err
|
||||
}
|
||||
|
||||
var _ api.MeterCurrent = (*GoE)(nil)
|
||||
|
||||
// Currents implements the api.MeterCurrent interface
|
||||
func (c *GoE) Currents() (float64, float64, float64, error) {
|
||||
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
|
||||
resp, err := c.api.Status()
|
||||
if err != nil {
|
||||
return 0, 0, 0, err
|
||||
}
|
||||
return 0, 0, 0, err
|
||||
|
||||
i1, i2, i3 := resp.Currents()
|
||||
|
||||
return i1, i2, i3, err
|
||||
}
|
||||
|
||||
var _ api.Identifier = (*GoE)(nil)
|
||||
|
||||
// Identify implements the api.Identifier interface
|
||||
func (c *GoE) Identify() (string, error) {
|
||||
status, err := c.apiStatus()
|
||||
return status.RFIDName(), err
|
||||
resp, err := c.api.Status()
|
||||
return resp.Identify(), err
|
||||
}
|
||||
|
||||
// totalEnergy implements the api.MeterEnergy interface - v2 only
|
||||
func (c *GoE) totalEnergy() (float64, error) {
|
||||
resp, err := c.api.Status()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
var val float64
|
||||
if res, ok := resp.(*goe.StatusResponse2); ok {
|
||||
val = res.TotalEnergy()
|
||||
}
|
||||
|
||||
return val, err
|
||||
}
|
||||
|
||||
// phases1p3p implements the api.ChargePhases interface - v2 only
|
||||
func (c *GoE) phases1p3p(phases int) error {
|
||||
if phases == 3 {
|
||||
phases = 2
|
||||
}
|
||||
|
||||
return c.api.Update(fmt.Sprintf("psm=%d", phases))
|
||||
}
|
||||
|
|
|
|||
160
charger/go-e/api.go
Normal file
160
charger/go-e/api.go
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
package goe
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/evcc-io/evcc/util"
|
||||
"github.com/evcc-io/evcc/util/request"
|
||||
)
|
||||
|
||||
const CloudURI = "https://api.go-e.co"
|
||||
|
||||
// Response is the v1 and v2 api response interface
|
||||
type Response interface {
|
||||
Status() int
|
||||
Enabled() bool
|
||||
CurrentPower() float64
|
||||
ChargedEnergy() float64
|
||||
Currents() (float64, float64, float64)
|
||||
Identify() string
|
||||
}
|
||||
|
||||
type UpdateResponse map[string]interface{}
|
||||
|
||||
type API interface {
|
||||
IsV2() bool
|
||||
Status() (Response, error)
|
||||
Update(payload string) error
|
||||
}
|
||||
|
||||
type LocalAPI struct {
|
||||
*request.Helper
|
||||
uri string
|
||||
v2 bool
|
||||
}
|
||||
|
||||
var _ API = (*LocalAPI)(nil)
|
||||
|
||||
func NewLocal(log *util.Logger, uri string) *LocalAPI {
|
||||
uri = strings.TrimRight(uri, "/")
|
||||
uri = strings.TrimSuffix(uri, "/api")
|
||||
|
||||
api := &LocalAPI{
|
||||
Helper: request.NewHelper(log),
|
||||
uri: uri,
|
||||
}
|
||||
|
||||
api.upgradeV2()
|
||||
|
||||
return api
|
||||
}
|
||||
|
||||
// upgradeV2 will switch to use the v2 api and revert if not available
|
||||
func (c *LocalAPI) upgradeV2() {
|
||||
c.v2 = true // use v2 response struct
|
||||
|
||||
res := new(StatusResponse2)
|
||||
err := c.response("api/status?filter=alw", &res)
|
||||
|
||||
if err == nil {
|
||||
c.uri = c.uri + "/api"
|
||||
} else {
|
||||
c.v2 = false
|
||||
}
|
||||
}
|
||||
|
||||
// IsV2 returns v2 api usage
|
||||
func (c *LocalAPI) IsV2() bool {
|
||||
return c.v2
|
||||
}
|
||||
|
||||
// response returns a v1/v2 api response
|
||||
func (c *LocalAPI) response(partial string, res interface{}) error {
|
||||
url := fmt.Sprintf("%s/%s", c.uri, partial)
|
||||
return c.GetJSON(url, &res)
|
||||
}
|
||||
|
||||
// Status reads a v1/v2 api response
|
||||
func (c *LocalAPI) Status() (Response, error) {
|
||||
if c.v2 {
|
||||
res := new(StatusResponse2)
|
||||
err := c.response("status?filter=alw,car,eto,nrg,wh,trx,cards", &res)
|
||||
return res, err
|
||||
}
|
||||
|
||||
res := new(StatusResponse)
|
||||
err := c.response("status", &res)
|
||||
return res, err
|
||||
}
|
||||
|
||||
// Update executes a v1/v2 api update and returns the response
|
||||
func (c *LocalAPI) Update(payload string) error {
|
||||
res := new(UpdateResponse)
|
||||
|
||||
if c.v2 {
|
||||
err := c.response(fmt.Sprintf("set?%s", payload), &res)
|
||||
return err
|
||||
}
|
||||
|
||||
err := c.response(fmt.Sprintf("mqtt?payload=%s", payload), &res)
|
||||
return err
|
||||
}
|
||||
|
||||
type cloud struct {
|
||||
*request.Helper
|
||||
token string
|
||||
cache time.Duration
|
||||
updated time.Time
|
||||
status Response
|
||||
}
|
||||
|
||||
var _ API = (*cloud)(nil)
|
||||
|
||||
func NewCloud(log *util.Logger, token string, cache time.Duration) API {
|
||||
return &cloud{
|
||||
Helper: request.NewHelper(log),
|
||||
token: token,
|
||||
cache: cache,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *cloud) IsV2() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (c *cloud) response(function, payload string) (*StatusResponse, error) {
|
||||
var status CloudResponse
|
||||
|
||||
url := fmt.Sprintf("%s/%s?token=%s", CloudURI, 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 *cloud) Status() (status Response, err error) {
|
||||
if time.Since(c.updated) >= c.cache {
|
||||
status, err = c.response("api_status", "")
|
||||
if err == nil {
|
||||
c.updated = time.Now()
|
||||
c.status = status
|
||||
}
|
||||
}
|
||||
|
||||
return c.status, err
|
||||
}
|
||||
|
||||
func (c *cloud) Update(payload string) error {
|
||||
c.updated = time.Time{}
|
||||
_, err := c.response("api", payload)
|
||||
return err
|
||||
}
|
||||
68
charger/go-e/api_test.go
Normal file
68
charger/go-e/api_test.go
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
package goe
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/evcc-io/evcc/util"
|
||||
)
|
||||
|
||||
type handler struct {
|
||||
uri string
|
||||
}
|
||||
|
||||
func (h *handler) expect(uri string) {
|
||||
h.uri = uri
|
||||
}
|
||||
|
||||
func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
path := r.URL.Path
|
||||
if r.URL.RawQuery != "" {
|
||||
path += "?" + r.URL.RawQuery
|
||||
}
|
||||
fmt.Println(path)
|
||||
|
||||
if path == h.uri {
|
||||
fmt.Fprint(w, "{}")
|
||||
} else {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
fmt.Fprintf(w, "expected %s", h.uri)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLocalV1(t *testing.T) {
|
||||
h := &handler{}
|
||||
srv := httptest.NewServer(h)
|
||||
|
||||
// h.expect("/api/status?filter=alw")
|
||||
local := NewLocal(util.NewLogger("foo"), srv.URL)
|
||||
|
||||
h.expect("/status")
|
||||
if _, err := local.Status(); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
h.expect("/mqtt?payload=foo=bar")
|
||||
if err := local.Update("foo=bar"); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
func TestLocalV2(t *testing.T) {
|
||||
h := &handler{}
|
||||
srv := httptest.NewServer(h)
|
||||
|
||||
h.expect("/api/status?filter=alw")
|
||||
local := NewLocal(util.NewLogger("foo"), srv.URL)
|
||||
|
||||
h.expect("/api/status?filter=alw,car,eto,nrg,wh,trx,cards")
|
||||
if _, err := local.Status(); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
h.expect("/api/set?foo=bar")
|
||||
if err := local.Update("foo=bar"); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
86
charger/go-e/types.go
Normal file
86
charger/go-e/types.go
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
package goe
|
||||
|
||||
// CloudResponse is the cloud API response
|
||||
type CloudResponse struct {
|
||||
Success *bool // only valid for cloud payload commands
|
||||
Age int
|
||||
Error string // only valid for cloud payload commands
|
||||
Data StatusResponse
|
||||
}
|
||||
|
||||
// StatusResponse is the API response if status not OK
|
||||
type StatusResponse struct {
|
||||
Fwv string `json:"fwv"` // firmware version
|
||||
Car int `json:"car,string"` // car status
|
||||
Alw int `json:"alw,string"` // allow charging
|
||||
Amp int `json:"amp,string"` // current [A]
|
||||
Err int `json:"err,string"` // error
|
||||
Stp int `json:"stp,string"` // stop state
|
||||
Tmp int `json:"tmp,string"` // temperature [°C]
|
||||
Dws int `json:"dws,string"` // energy [Ws]
|
||||
Nrg []float64 `json:"nrg"` // voltage, current, power
|
||||
Uby int `json:"uby,string"` // unlocked_by
|
||||
Rna string `json:"rna"` // RFID 1
|
||||
Rnm string `json:"rnm"` // RFID 2
|
||||
Rne string `json:"rne"` // RFID 3
|
||||
Rn4 string `json:"rn4"` // RFID 4
|
||||
Rn5 string `json:"rn5"` // RFID 5
|
||||
Rn6 string `json:"rn6"` // RFID 6
|
||||
Rn7 string `json:"rn7"` // RFID 7
|
||||
Rn8 string `json:"rn8"` // RFID 8
|
||||
Rn9 string `json:"rn9"` // RFID 9
|
||||
Rn1 string `json:"rn1"` // RFID 10
|
||||
}
|
||||
|
||||
func (g *StatusResponse) Status() int {
|
||||
return g.Car
|
||||
}
|
||||
|
||||
func (g *StatusResponse) Enabled() bool {
|
||||
return g.Alw == 1
|
||||
}
|
||||
|
||||
func (g *StatusResponse) CurrentPower() float64 {
|
||||
if len(g.Nrg) == 16 {
|
||||
return g.Nrg[11] * 10
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (g *StatusResponse) ChargedEnergy() float64 {
|
||||
return float64(g.Dws) / 3.6e5 // Deka-Watt-Seconds to kWh (100.000 == 0,277kWh)
|
||||
}
|
||||
|
||||
func (g *StatusResponse) Currents() (float64, float64, float64) {
|
||||
if len(g.Nrg) == 16 {
|
||||
return g.Nrg[4] / 10, g.Nrg[5] / 10, g.Nrg[6] / 10
|
||||
}
|
||||
return 0, 0, 0
|
||||
}
|
||||
|
||||
func (g *StatusResponse) Identify() string {
|
||||
switch g.Uby {
|
||||
case 1:
|
||||
return g.Rna
|
||||
case 2:
|
||||
return g.Rnm
|
||||
case 3:
|
||||
return g.Rne
|
||||
case 4:
|
||||
return g.Rn4
|
||||
case 5:
|
||||
return g.Rn5
|
||||
case 6:
|
||||
return g.Rn6
|
||||
case 7:
|
||||
return g.Rn7
|
||||
case 8:
|
||||
return g.Rn8
|
||||
case 9:
|
||||
return g.Rn9
|
||||
case 10:
|
||||
return g.Rn1
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
65
charger/go-e/types2.go
Normal file
65
charger/go-e/types2.go
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
package goe
|
||||
|
||||
// StatusResponse2 is the v2 API response
|
||||
type StatusResponse2 struct {
|
||||
Fwv string // firmware version
|
||||
Car int // car status
|
||||
Alw bool // allow charging
|
||||
Amp int // current [A]
|
||||
Err int // error
|
||||
Eto uint64 // energy total Wh
|
||||
Psm int // phase switching
|
||||
Stp int // stop state
|
||||
Tmp int // temperature [°C]
|
||||
Trx int // transaction
|
||||
Nrg []float64 // voltage, current, power
|
||||
Wh float64 // energy [Wh]
|
||||
Cards []Card // RFID cards
|
||||
}
|
||||
|
||||
// Card is the v2 RFID card
|
||||
type Card struct {
|
||||
Name string
|
||||
Energy int
|
||||
CardID bool
|
||||
}
|
||||
|
||||
func (g *StatusResponse2) Status() int {
|
||||
return g.Car
|
||||
}
|
||||
|
||||
func (g *StatusResponse2) Enabled() bool {
|
||||
return g.Alw
|
||||
}
|
||||
|
||||
func (g *StatusResponse2) CurrentPower() float64 {
|
||||
if len(g.Nrg) == 16 {
|
||||
return g.Nrg[11]
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
func (g *StatusResponse2) ChargedEnergy() float64 {
|
||||
return g.Wh / 1e3
|
||||
}
|
||||
|
||||
func (g *StatusResponse2) TotalEnergy() float64 {
|
||||
return float64(g.Eto) / 1e3
|
||||
}
|
||||
|
||||
func (g *StatusResponse2) Currents() (float64, float64, float64) {
|
||||
if len(g.Nrg) == 16 {
|
||||
return g.Nrg[4], g.Nrg[5], g.Nrg[6]
|
||||
}
|
||||
|
||||
return 0, 0, 0
|
||||
}
|
||||
|
||||
func (g *StatusResponse2) Identify() string {
|
||||
if g.Trx > 0 {
|
||||
return g.Cards[g.Trx-1].Name
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
69
charger/go-e_decorators.go
Normal file
69
charger/go-e_decorators.go
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
package charger
|
||||
|
||||
// Code generated by github.com/andig/cmd/tools/decorate.go. DO NOT EDIT.
|
||||
|
||||
import (
|
||||
"github.com/evcc-io/evcc/api"
|
||||
)
|
||||
|
||||
func decorateGoE(base *GoE, meterEnergy func() (float64, error), chargePhases func(phases int) error) api.Charger {
|
||||
switch {
|
||||
case chargePhases == nil && meterEnergy == nil:
|
||||
return base
|
||||
|
||||
case chargePhases == nil && meterEnergy != nil:
|
||||
return &struct {
|
||||
*GoE
|
||||
api.MeterEnergy
|
||||
}{
|
||||
GoE: base,
|
||||
MeterEnergy: &decorateGoEMeterEnergyImpl{
|
||||
meterEnergy: meterEnergy,
|
||||
},
|
||||
}
|
||||
|
||||
case chargePhases != nil && meterEnergy == nil:
|
||||
return &struct {
|
||||
*GoE
|
||||
api.ChargePhases
|
||||
}{
|
||||
GoE: base,
|
||||
ChargePhases: &decorateGoEChargePhasesImpl{
|
||||
chargePhases: chargePhases,
|
||||
},
|
||||
}
|
||||
|
||||
case chargePhases != nil && meterEnergy != nil:
|
||||
return &struct {
|
||||
*GoE
|
||||
api.ChargePhases
|
||||
api.MeterEnergy
|
||||
}{
|
||||
GoE: base,
|
||||
ChargePhases: &decorateGoEChargePhasesImpl{
|
||||
chargePhases: chargePhases,
|
||||
},
|
||||
MeterEnergy: &decorateGoEMeterEnergyImpl{
|
||||
meterEnergy: meterEnergy,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type decorateGoEChargePhasesImpl struct {
|
||||
chargePhases func(int) error
|
||||
}
|
||||
|
||||
func (impl *decorateGoEChargePhasesImpl) Phases1p3p(phases int) error {
|
||||
return impl.chargePhases(phases)
|
||||
}
|
||||
|
||||
type decorateGoEMeterEnergyImpl struct {
|
||||
meterEnergy func() (float64, error)
|
||||
}
|
||||
|
||||
func (impl *decorateGoEMeterEnergyImpl) TotalEnergy() (float64, error) {
|
||||
return impl.meterEnergy()
|
||||
}
|
||||
89
charger/go-e_test.go
Normal file
89
charger/go-e_test.go
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
package charger
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/evcc-io/evcc/api"
|
||||
"github.com/evcc-io/evcc/util/sponsor"
|
||||
)
|
||||
|
||||
type handler struct {
|
||||
uri string
|
||||
}
|
||||
|
||||
func (h *handler) expect(uri string) {
|
||||
h.uri = uri
|
||||
}
|
||||
|
||||
func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
path := r.URL.Path
|
||||
if r.URL.RawQuery != "" {
|
||||
path += "?" + r.URL.RawQuery
|
||||
}
|
||||
fmt.Println(path)
|
||||
|
||||
if path == h.uri {
|
||||
fmt.Fprint(w, "{}")
|
||||
} else {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
fmt.Fprintf(w, "expected %s", h.uri)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGoEV1(t *testing.T) {
|
||||
h := &handler{}
|
||||
srv := httptest.NewServer(h)
|
||||
|
||||
wb, err := NewGoE(srv.URL, "", 0)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
if _, ok := wb.(api.Meter); !ok {
|
||||
t.Error("missing Meter api")
|
||||
}
|
||||
|
||||
if _, ok := wb.(api.MeterCurrent); !ok {
|
||||
t.Error("missing MeterCurrent api")
|
||||
}
|
||||
|
||||
if _, ok := wb.(api.Identifier); !ok {
|
||||
t.Error("missing Identifier api")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGoEV2(t *testing.T) {
|
||||
h := &handler{}
|
||||
srv := httptest.NewServer(h)
|
||||
|
||||
sponsor.Subject = "foo"
|
||||
|
||||
h.expect("/api/status?filter=alw")
|
||||
wb, err := NewGoE(srv.URL, "", 0)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
if _, ok := wb.(api.Meter); !ok {
|
||||
t.Error("missing Meter api")
|
||||
}
|
||||
|
||||
if _, ok := wb.(api.MeterCurrent); !ok {
|
||||
t.Error("missing MeterCurrent api")
|
||||
}
|
||||
|
||||
if _, ok := wb.(api.Identifier); !ok {
|
||||
t.Error("missing Identifier api")
|
||||
}
|
||||
|
||||
if _, ok := wb.(api.MeterEnergy); !ok {
|
||||
t.Error("missing MeterEnergy api")
|
||||
}
|
||||
|
||||
if _, ok := wb.(api.ChargePhases); !ok {
|
||||
t.Error("missing ChargePhases api")
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue