Add Mobile Charger Connect (#40)
This commit is contained in:
parent
018836d663
commit
fe612eb00b
6 changed files with 695 additions and 1 deletions
|
|
@ -7,7 +7,7 @@ EVCC is an extensible EV Charge Controller with PV integration implemented in [G
|
|||
### Features
|
||||
|
||||
- simple and clean user interface
|
||||
- multiple [chargers](#charger): Wallbe (tested with Wallbe Eco S), Phoenix controllers (similar to Wallbe), go-eCharger, openWB slave, any other charger using scripting
|
||||
- multiple [chargers](#charger): Wallbe (tested with Wallbe Eco S), Phoenix controllers (similar to Wallbe), go-eCharger, openWB slave, Mobile Charger Connect (currently used by Porsche), any other charger using scripting
|
||||
- more chargers experimentally supported: NRGKick, SimpleEVSE, EVSEWifi
|
||||
- different [vehicles](#vehicle) to show battery status: Audi (eTron), BMW (i3), Tesla, Nissan (Leaf), any other vehicle using scripting
|
||||
- integration with home automation - supports shell scripts and MQTT
|
||||
|
|
@ -115,6 +115,7 @@ Available charger implementations are:
|
|||
- `evsewifi`: chargers with SimpleEVSE controllers using [SimpleEVSE-Wifi](https://github.com/CurtRod/SimpleEVSE-WiFi)
|
||||
- `nrgkick`: NRGKick chargers with Connect module
|
||||
- `go-e`: go-eCharger chargers
|
||||
- `mcc`: Mobile Charger Connect devices (Audi, Bentley, Porsche)
|
||||
- `default`: default charger implementation using configurable [plugins](#plugins) for integrating any type of charger
|
||||
|
||||
#### Wallbe Hardware Preparation
|
||||
|
|
|
|||
|
|
@ -54,6 +54,12 @@ func (r *HTTPHelper) decodeJSON(resp *http.Response, err error, res interface{})
|
|||
return b, err
|
||||
}
|
||||
|
||||
// Request executes HTTP request returns the response body
|
||||
func (r *HTTPHelper) Request(req *http.Request) ([]byte, error) {
|
||||
resp, err := r.Client.Do(req)
|
||||
return r.readBody(resp, err)
|
||||
}
|
||||
|
||||
// Get executes HTTP GET request returns the response body
|
||||
func (r *HTTPHelper) Get(url string) ([]byte, error) {
|
||||
resp, err := r.Client.Get(url)
|
||||
|
|
|
|||
|
|
@ -27,6 +27,8 @@ func NewFromConfig(log *api.Logger, typ string, other map[string]interface{}) ap
|
|||
c = NewEVSEWifiFromConfig(log, other)
|
||||
case "simpleevse", "evse":
|
||||
c = NewSimpleEVSEFromConfig(log, other)
|
||||
case "porsche", "audi", "bentley", "mcc":
|
||||
c = NewMobileConnectFromConfig(log, other)
|
||||
default:
|
||||
log.FATAL.Fatalf("invalid charger type '%s'", typ)
|
||||
}
|
||||
|
|
|
|||
333
charger/mcc.go
Normal file
333
charger/mcc.go
Normal file
|
|
@ -0,0 +1,333 @@
|
|||
package charger
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/andig/evcc/api"
|
||||
)
|
||||
|
||||
const (
|
||||
apiLogin apiFunction = "jwt/login"
|
||||
apiRefresh apiFunction = "jwt/refresh"
|
||||
apiChargeState apiFunction = "v1/api/WebServer/properties/chargeState"
|
||||
apiCurrentSession apiFunction = "v1/api/WebServer/properties/swaggerCurrentSession"
|
||||
apiEnergy apiFunction = "v1/api/iCAN/properties/propjIcanEnergy"
|
||||
apiSetCurrentLimit apiFunction = "v1/api/SCC/properties/propHMICurrentLimit?value="
|
||||
apiCurrentCableInformation apiFunction = "v1/api/SCC/properties/json_CurrentCableInformation"
|
||||
)
|
||||
|
||||
// MCCErrorResponse is the API response if status not OK
|
||||
type MCCErrorResponse struct {
|
||||
Error string
|
||||
}
|
||||
|
||||
// MCCTokenResponse is the apiLogin response
|
||||
type MCCTokenResponse struct {
|
||||
Token string
|
||||
}
|
||||
|
||||
// MCCCurrentSession is the apiCurrentSession response
|
||||
type MCCCurrentSession struct {
|
||||
Duration time.Duration
|
||||
EnergySumKwh float64
|
||||
}
|
||||
|
||||
// MCCEnergyPhase is the apiEnergy response for a single phase
|
||||
type MCCEnergyPhase struct {
|
||||
Power float64
|
||||
}
|
||||
|
||||
// MCCEnergy is the apiEnergy response
|
||||
type MCCEnergy struct {
|
||||
L1, L2, L3 MCCEnergyPhase
|
||||
}
|
||||
|
||||
// MCCCurrentCableInformation is the apiCurrentCableInformation response
|
||||
type MCCCurrentCableInformation struct {
|
||||
MaxValue, MinValue, Value int64
|
||||
}
|
||||
|
||||
// MobileConnect charger supporting devices from Audi, Bentley, Porsche
|
||||
type MobileConnect struct {
|
||||
*api.HTTPHelper
|
||||
uri string
|
||||
password string
|
||||
token string
|
||||
tokenValid time.Time
|
||||
tokenRefresh time.Time
|
||||
cableInformation MCCCurrentCableInformation
|
||||
}
|
||||
|
||||
// NewMobileConnectFromConfig creates a MCC charger from generic config
|
||||
func NewMobileConnectFromConfig(log *api.Logger, other map[string]interface{}) api.Charger {
|
||||
cc := struct{ URI, Password string }{}
|
||||
api.DecodeOther(log, other, &cc)
|
||||
|
||||
return NewMobileConnect(cc.URI, cc.Password)
|
||||
}
|
||||
|
||||
// NewMobileConnect creates MCC charger
|
||||
func NewMobileConnect(uri string, password string) *MobileConnect {
|
||||
mcc := &MobileConnect{
|
||||
HTTPHelper: api.NewHTTPHelper(api.NewLogger("mcc ")),
|
||||
uri: strings.TrimRight(uri, "/"),
|
||||
password: password,
|
||||
}
|
||||
|
||||
// ignore the self signed certificate
|
||||
customTransport := http.DefaultTransport.(*http.Transport).Clone()
|
||||
customTransport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
|
||||
|
||||
mcc.HTTPHelper.Client.Transport = customTransport
|
||||
|
||||
return mcc
|
||||
}
|
||||
|
||||
// construct the URL for a given apiFunction
|
||||
func (mcc *MobileConnect) apiURL(api apiFunction) string {
|
||||
return fmt.Sprintf("%s/%s", mcc.uri, api)
|
||||
}
|
||||
|
||||
// process the http request to fetch the auth token for a login or refresh request
|
||||
func (mcc *MobileConnect) fetchToken(request *http.Request) error {
|
||||
var tr MCCTokenResponse
|
||||
b, err := mcc.RequestJSON(request, &tr)
|
||||
if err == nil {
|
||||
if len(tr.Token) == 0 && len(b) > 0 {
|
||||
var error MCCErrorResponse
|
||||
|
||||
if err := json.Unmarshal(b, &error); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return fmt.Errorf("response: %s", error.Error)
|
||||
}
|
||||
|
||||
mcc.token = tr.Token
|
||||
// According to the Web Interface, the token is valid for 10 minutes
|
||||
mcc.tokenValid = time.Now().Add(10 * time.Minute)
|
||||
|
||||
// the web interface updates the token every 2 minutes, so lets do the same here
|
||||
mcc.tokenRefresh = time.Now().Add(2 * time.Minute)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// login as the home user with the given password
|
||||
func (mcc *MobileConnect) login(password string) error {
|
||||
uri := fmt.Sprintf("%s/%s", mcc.uri, apiLogin)
|
||||
|
||||
data := url.Values{
|
||||
"user": []string{"user"},
|
||||
"pass": []string{mcc.password},
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(http.MethodPost, uri, strings.NewReader(data.Encode()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
|
||||
return mcc.fetchToken(req)
|
||||
}
|
||||
|
||||
// refresh the auth token with a new one
|
||||
func (mcc *MobileConnect) refresh() error {
|
||||
uri := fmt.Sprintf("%s/%s", mcc.uri, apiRefresh)
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, uri, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", mcc.token))
|
||||
|
||||
return mcc.fetchToken(req)
|
||||
}
|
||||
|
||||
// creates a http request that contains the auth token
|
||||
func (mcc *MobileConnect) request(method, uri string) (*http.Request, error) {
|
||||
// do we need to login?
|
||||
if mcc.token == "" || time.Since(mcc.tokenValid) > 0 {
|
||||
if err := mcc.login(mcc.password); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// is it time to refresh the token?
|
||||
if time.Since(mcc.tokenRefresh) > 0 {
|
||||
if err := mcc.refresh(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// now lets process the request with the fetched token
|
||||
req, err := http.NewRequest(method, uri, nil)
|
||||
if err != nil {
|
||||
return req, err
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", mcc.token))
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
// use http GET to fetch a non structured value from an URI and stores it in result
|
||||
func (mcc *MobileConnect) getValue(uri string) ([]byte, error) {
|
||||
req, err := mcc.request(http.MethodGet, uri)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return mcc.Request(req)
|
||||
}
|
||||
|
||||
// use http GET to fetch an escaped JSON string and unmarshal the data in result
|
||||
func (mcc *MobileConnect) getEscapedJSON(uri string, result interface{}) error {
|
||||
req, err := mcc.request(http.MethodGet, uri)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
b, err := mcc.Request(req)
|
||||
if err == nil {
|
||||
var s string
|
||||
if s, err = strconv.Unquote(strings.Trim(string(b), "\n")); err == nil {
|
||||
err = json.Unmarshal([]byte(s), &result)
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// Status implements the Charger.Status interface
|
||||
func (mcc *MobileConnect) Status() (api.ChargeStatus, error) {
|
||||
b, err := mcc.getValue(mcc.apiURL(apiChargeState))
|
||||
if err != nil {
|
||||
return api.StatusNone, err
|
||||
}
|
||||
|
||||
chargeState, err := strconv.ParseInt(strings.Trim(string(b), "\n"), 10, 8)
|
||||
if err != nil {
|
||||
return api.StatusNone, err
|
||||
}
|
||||
|
||||
switch chargeState {
|
||||
case 0: // Unplugged
|
||||
return api.StatusA, nil
|
||||
case 1, 3, 4, 6: // 1: Connecting, 3: Established, 4: Paused, 6: Finished
|
||||
return api.StatusB, nil
|
||||
case 2: // Error
|
||||
return api.StatusF, nil
|
||||
case 5: // Active
|
||||
return api.StatusC, nil
|
||||
default:
|
||||
return api.StatusNone, fmt.Errorf("properties unknown result: %d", chargeState)
|
||||
}
|
||||
}
|
||||
|
||||
// Enabled implements the Charger.Enabled interface
|
||||
func (mcc *MobileConnect) Enabled() (bool, error) {
|
||||
// Check if the car is connected and Paused, Active, or Finished
|
||||
b, err := mcc.getValue(mcc.apiURL(apiChargeState))
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// return value is returned in the format 0\n
|
||||
chargeState, err := strconv.ParseInt(strings.Trim(string(b), "\n"), 10, 8)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if chargeState >= 4 && chargeState <= 6 {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Enable implements the Charger.Enable interface
|
||||
func (mcc *MobileConnect) Enable(enable bool) error {
|
||||
// As we don't know of the API to disable charging this for now always returns an error
|
||||
return nil
|
||||
}
|
||||
|
||||
// MaxCurrent implements the Charger.MaxCurrent interface
|
||||
func (mcc *MobileConnect) MaxCurrent(current int64) error {
|
||||
// The device doesn't return an error if we set a value greater than the
|
||||
// current allowed max or smaller than the allowed min
|
||||
// instead it will simply set it to max or min and return "OK" anyway
|
||||
// Since the API here works differently, we fetch the limits
|
||||
// and then return an error if the value is outside of the limits or
|
||||
// otherwise set the new value
|
||||
if mcc.cableInformation.MaxValue == 0 {
|
||||
if err := mcc.getEscapedJSON(mcc.apiURL(apiCurrentCableInformation), &mcc.cableInformation); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if current < mcc.cableInformation.MinValue {
|
||||
return fmt.Errorf("value is lower than the allowed minimum value %d", mcc.cableInformation.MinValue)
|
||||
}
|
||||
|
||||
if current > mcc.cableInformation.MaxValue {
|
||||
return fmt.Errorf("value is higher than the allowed maximum value %d", mcc.cableInformation.MaxValue)
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("%s%d", mcc.apiURL(apiSetCurrentLimit), current)
|
||||
|
||||
req, err := mcc.request(http.MethodPut, url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
b, err := mcc.Request(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// return value is returned in the format "OK"\n
|
||||
if strings.Trim(string(b), "\n\"") != "OK" {
|
||||
return fmt.Errorf("maxcurrent unexpected response: %s", string(b))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CurrentPower implements the Meter interface.
|
||||
func (mcc *MobileConnect) CurrentPower() (float64, error) {
|
||||
var energy MCCEnergy
|
||||
err := mcc.getEscapedJSON(mcc.apiURL(apiEnergy), &energy)
|
||||
|
||||
return energy.L1.Power + energy.L2.Power + energy.L3.Power, err
|
||||
}
|
||||
|
||||
// ChargedEnergy implements the ChargeRater interface.
|
||||
func (mcc *MobileConnect) ChargedEnergy() (float64, error) {
|
||||
var currentSession MCCCurrentSession
|
||||
if err := mcc.getEscapedJSON(mcc.apiURL(apiCurrentSession), ¤tSession); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return currentSession.EnergySumKwh, nil
|
||||
}
|
||||
|
||||
// ChargingTime yields current charge run duration
|
||||
func (mcc *MobileConnect) ChargingTime() (time.Duration, error) {
|
||||
var currentSession MCCCurrentSession
|
||||
if err := mcc.getEscapedJSON(mcc.apiURL(apiCurrentSession), ¤tSession); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return time.Duration(currentSession.Duration * time.Second), nil
|
||||
}
|
||||
348
charger/mcc_test.go
Normal file
348
charger/mcc_test.go
Normal file
|
|
@ -0,0 +1,348 @@
|
|||
package charger
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/andig/evcc/api"
|
||||
)
|
||||
|
||||
// HTTP testing appproach from http://hassansin.github.io/Unit-Testing-http-client-in-Go
|
||||
type roundTripFunc func(req *http.Request) *http.Response
|
||||
|
||||
// RoundTrip .
|
||||
func (f roundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
return f(req), nil
|
||||
}
|
||||
|
||||
// apiResponse helps to map an API Call to a test response
|
||||
type apiResponse struct {
|
||||
apiCall apiFunction
|
||||
apiResponse string
|
||||
}
|
||||
|
||||
// NewTestClient returns *http.Client with Transport replaced to avoid making real calls
|
||||
func NewTestClient(fn roundTripFunc) *http.Client {
|
||||
return &http.Client{
|
||||
Transport: roundTripFunc(fn),
|
||||
}
|
||||
}
|
||||
|
||||
// NewTestMobileConnect .
|
||||
func NewTestMobileConnect(t *testing.T, responses []apiResponse) *MobileConnect {
|
||||
mcc := &MobileConnect{
|
||||
HTTPHelper: api.NewHTTPHelper(nil),
|
||||
uri: "http://192.168.1.1",
|
||||
password: "none",
|
||||
token: "token",
|
||||
tokenValid: time.Now().Add(10 * time.Minute),
|
||||
tokenRefresh: time.Now().Add(10 * time.Minute),
|
||||
}
|
||||
|
||||
mcc.Client = NewTestClient(func(req *http.Request) *http.Response {
|
||||
// Each method may have multiple API calls, so we need to finde the proper
|
||||
// response string for the currently invoked call
|
||||
var responseString string
|
||||
for _, s := range responses {
|
||||
if strings.Contains("/"+string(s.apiCall), req.URL.Path) {
|
||||
responseString = s.apiResponse
|
||||
}
|
||||
}
|
||||
|
||||
return &http.Response{
|
||||
StatusCode: 200,
|
||||
// Send response to be tested
|
||||
Body: ioutil.NopCloser(bytes.NewBufferString(responseString)),
|
||||
// Must be set to non-nil value or it panics
|
||||
Header: make(http.Header),
|
||||
}
|
||||
})
|
||||
|
||||
return mcc
|
||||
}
|
||||
|
||||
func TestMobileConnect_login(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
responses []apiResponse
|
||||
password string
|
||||
wantErr bool
|
||||
}{
|
||||
// test cases for software version 2914
|
||||
{"login - success", []apiResponse{{apiLogin, "{\n \"token\": \"1234567890._abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ\"\n}"}}, "password", false},
|
||||
{"login - wrong password", []apiResponse{{apiLogin, "{\n \"error\": \"wrong password\"\n}"}}, "wrong", true},
|
||||
{"login - bad return", []apiResponse{{apiLogin, "{{\n \"error\": \"wrong password\"\n}"}}, "wrong", true},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
mcc := NewTestMobileConnect(t, tc.responses)
|
||||
|
||||
if err := mcc.login(tc.password); (err != nil) != tc.wantErr {
|
||||
t.Errorf("MobileConnect.login() error = %v, wantErr %v", err, tc.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMobileConnect_refresh(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
responses []apiResponse
|
||||
wantErr bool
|
||||
}{
|
||||
// test cases for software version 2914
|
||||
{"refresh - success", []apiResponse{{apiRefresh, "{\n \"token\": \"1234567890._abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ\"\n}"}}, false},
|
||||
{"refresh - wrong password", []apiResponse{{apiRefresh, "{\n \"error\": \"signature mismatch: OP-gWPOgQ9fdKujMgRNHkeH4WHqYrHe3Z2RqVXeUEuw1\"\n}"}}, true},
|
||||
{"refresh - bad return", []apiResponse{{apiRefresh, "{{\n \"error\": \"\"\n}"}}, true},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
mcc := NewTestMobileConnect(t, tc.responses)
|
||||
|
||||
if err := mcc.refresh(); (err != nil) != tc.wantErr {
|
||||
t.Errorf("MobileConnect.login() error = %v, wantErr %v", err, tc.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMobileConnect_Status(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
responses []apiResponse
|
||||
want api.ChargeStatus
|
||||
wantErr bool
|
||||
}{
|
||||
// test cases for software version 2914
|
||||
{"home plug - Unexpected API response", []apiResponse{{apiChargeState, "abc"}}, api.StatusNone, true},
|
||||
{"home plug - Unplugged", []apiResponse{{apiChargeState, "0\n"}}, api.StatusA, false},
|
||||
{"home plug - Connecting", []apiResponse{{apiChargeState, "1\n"}}, api.StatusB, false},
|
||||
{"home plug - Error", []apiResponse{{apiChargeState, "2\n"}}, api.StatusF, false},
|
||||
{"home plug - Established", []apiResponse{{apiChargeState, "3\n"}}, api.StatusB, false},
|
||||
{"home plug - Paused", []apiResponse{{apiChargeState, "4\n"}}, api.StatusB, false},
|
||||
{"home plug - Active", []apiResponse{{apiChargeState, "5\n"}}, api.StatusC, false},
|
||||
{"home plug - Finished", []apiResponse{{apiChargeState, "6\n"}}, api.StatusB, false},
|
||||
{"home plug - Unexpected status value", []apiResponse{{apiChargeState, "10\n"}}, api.StatusNone, true},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
mcc := NewTestMobileConnect(t, tc.responses)
|
||||
|
||||
got, err := mcc.Status()
|
||||
if (err != nil) != tc.wantErr {
|
||||
t.Errorf("MobileConnect.Status() error = %v, wantErr %v", err, tc.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tc.want) {
|
||||
t.Errorf("MobileConnect.Status() = %v, want %v", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMobileConnect_Enabled(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
responses []apiResponse
|
||||
want bool
|
||||
wantErr bool
|
||||
}{
|
||||
// test cases for software version 2914
|
||||
{"home plug - Unexpected API response", []apiResponse{{apiChargeState, "abc"}}, false, true},
|
||||
{"home plug - Unplugged", []apiResponse{{apiChargeState, "0\n"}}, false, false},
|
||||
{"home plug - Connecting", []apiResponse{{apiChargeState, "1\n"}}, false, false},
|
||||
{"home plug - Error", []apiResponse{{apiChargeState, "2\n"}}, false, false},
|
||||
{"home plug - Established", []apiResponse{{apiChargeState, "3\n"}}, false, false},
|
||||
{"home plug - Paused", []apiResponse{{apiChargeState, "4\n"}}, true, false},
|
||||
{"home plug - Active", []apiResponse{{apiChargeState, "5\n"}}, true, false},
|
||||
{"home plug - Finished", []apiResponse{{apiChargeState, "6\n"}}, true, false},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
mcc := NewTestMobileConnect(t, tc.responses)
|
||||
|
||||
got, err := mcc.Enabled()
|
||||
if (err != nil) != tc.wantErr {
|
||||
t.Errorf("MobileConnect.Enabled() error = %v, wantErr %v", err, tc.wantErr)
|
||||
return
|
||||
}
|
||||
if got != tc.want {
|
||||
t.Errorf("MobileConnect.Enabled() = %v, want %v", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMobileConnect_MaxCurrent(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
responses []apiResponse
|
||||
current int64
|
||||
wantErr bool
|
||||
}{
|
||||
// test cases for software version 2914
|
||||
{
|
||||
"home plug - success min value",
|
||||
[]apiResponse{
|
||||
{apiCurrentCableInformation, "\"{\\\"carCable\\\":5,\\\"gridCable\\\":8,\\\"hwfpMaxLimit\\\":32,\\\"maxValue\\\":10,\\\"minValue\\\":6,\\\"value\\\":10}\""},
|
||||
{apiSetCurrentLimit, "\"OK\"\n"},
|
||||
},
|
||||
6, false,
|
||||
},
|
||||
{
|
||||
"home plug - success max value",
|
||||
[]apiResponse{
|
||||
{apiCurrentCableInformation, "\"{\\\"carCable\\\":5,\\\"gridCable\\\":8,\\\"hwfpMaxLimit\\\":32,\\\"maxValue\\\":10,\\\"minValue\\\":6,\\\"value\\\":10}\""},
|
||||
{apiSetCurrentLimit, "\"OK\"\n"},
|
||||
},
|
||||
10, false,
|
||||
},
|
||||
{
|
||||
"home plug - error value too small",
|
||||
[]apiResponse{
|
||||
{apiCurrentCableInformation, "\"{\\\"carCable\\\":5,\\\"gridCable\\\":8,\\\"hwfpMaxLimit\\\":32,\\\"maxValue\\\":10,\\\"minValue\\\":6,\\\"value\\\":10}\""},
|
||||
},
|
||||
0, true,
|
||||
},
|
||||
{
|
||||
"home plug - error value too big",
|
||||
[]apiResponse{
|
||||
{apiCurrentCableInformation, "\"{\\\"carCable\\\":5,\\\"gridCable\\\":8,\\\"hwfpMaxLimit\\\":32,\\\"maxValue\\\":10,\\\"minValue\\\":6,\\\"value\\\":10}\""},
|
||||
},
|
||||
16, true,
|
||||
},
|
||||
{
|
||||
"home plug - 1st API success but 2nd API error",
|
||||
[]apiResponse{
|
||||
{apiCurrentCableInformation, "\"{\\\"carCable\\\":5,\\\"gridCable\\\":8,\\\"hwfpMaxLimit\\\":32,\\\"maxValue\\\":10,\\\"minValue\\\":6,\\\"value\\\":10}\""},
|
||||
{apiSetCurrentLimit, "Unexpected response"},
|
||||
},
|
||||
10, true,
|
||||
},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
mcc := NewTestMobileConnect(t, tc.responses)
|
||||
|
||||
if err := mcc.MaxCurrent(tc.current); (err != nil) != tc.wantErr {
|
||||
t.Errorf("MobileConnect.MaxCurrent() error = %v, wantErr %v", err, tc.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMobileConnect_CurrentPower(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
responses []apiResponse
|
||||
want float64
|
||||
wantErr bool
|
||||
}{
|
||||
// test cases for software version 2914
|
||||
{
|
||||
"home plug - charging",
|
||||
[]apiResponse{
|
||||
{apiEnergy, "\"{\\n \\\"L1\\\": {\\n \\\"Ampere\\\": 9.9000000000000004,\\n \\\"Power\\\": 2308,\\n \\\"Volts\\\": 230.5\\n },\\n \\\"L2\\\": {\\n \\\"Ampere\\\": 0,\\n \\\"Power\\\": 0,\\n \\\"Volts\\\": 13.700000000000001\\n },\\n \\\"L3\\\": {\\n \\\"Ampere\\\": 0,\\n \\\"Power\\\": 0,\\n \\\"Volts\\\": 13.9\\n }\\n}\\n\""},
|
||||
},
|
||||
2308, false,
|
||||
},
|
||||
{
|
||||
"home plug - error response",
|
||||
[]apiResponse{
|
||||
{apiEnergy, "\"{\\n \\\"L1\\\": {\\n \\\"Ampere\\\": 0,\\n \\\"Power\\\": 0,\\n \\\"Volts\\\": 246.60000000000002\\n },\\n \\\"L2\\\": {\\n \\\"Ampere\\\": 0,\\n \\\"Power\\\": 0,\\n \\\"Volts\\\": 16.800000000000001\\n },\\n \\\"L3\\\": {\\n \\\"Ampere\\\": 0,\\n \\\"Power\\\": 0,\\n \\\"Volts\\\": 16.300000000000001\\n }\\n}\\n\""},
|
||||
}, 0, false,
|
||||
},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
mcc := NewTestMobileConnect(t, tc.responses)
|
||||
|
||||
got, err := mcc.CurrentPower()
|
||||
if (err != nil) != tc.wantErr {
|
||||
t.Errorf("MobileConnect.CurrentPower() error = %v, wantErr %v", err, tc.wantErr)
|
||||
return
|
||||
}
|
||||
if got != tc.want {
|
||||
t.Errorf("MobileConnect.CurrentPower() = %v, want %v", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMobileConnect_ChargedEnergy(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
responses []apiResponse
|
||||
want float64
|
||||
wantErr bool
|
||||
}{
|
||||
// test cases for software version 2914
|
||||
{
|
||||
"valid response",
|
||||
[]apiResponse{
|
||||
{apiCurrentSession, "\"{\\n \\\"account\\\": \\\"PRIVATE\\\",\\n \\\"chargingRate\\\": 0,\\n \\\"chargingType\\\": \\\"AC\\\",\\n \\\"clockSrc\\\": \\\"NTP\\\",\\n \\\"costs\\\": 0,\\n \\\"currency\\\": \\\"\\\",\\n \\\"departTime\\\": \\\"\\\",\\n \\\"duration\\\": 30789,\\n \\\"endOfChargeTime\\\": \\\"\\\",\\n \\\"endSoc\\\": 0,\\n \\\"endTime\\\": \\\"\\\",\\n \\\"energySumKwh\\\": 18.832000000000001,\\n \\\"evChargingRatekW\\\": 0,\\n \\\"evTargetSoc\\\": -1,\\n \\\"evVasAvailability\\\": false,\\n \\\"pcid\\\": \\\"\\\",\\n \\\"powerRange\\\": 0,\\n \\\"selfEnergy\\\": 0,\\n \\\"sessionId\\\": 13,\\n \\\"soc\\\": -1,\\n \\\"solarEnergyShare\\\": 0,\\n \\\"startSoc\\\": 0,\\n \\\"startTime\\\": \\\"2020-04-15T10:07:22+02:00\\\",\\n \\\"totalRange\\\": 0,\\n \\\"vehicleBrand\\\": \\\"\\\",\\n \\\"vehicleModel\\\": \\\"\\\",\\n \\\"whitelist\\\": false\\n}\\n\""},
|
||||
}, 18.832000000000001, false,
|
||||
},
|
||||
{
|
||||
"error response",
|
||||
[]apiResponse{
|
||||
{apiCurrentSession, "\"\""},
|
||||
}, 0, true,
|
||||
},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
mcc := NewTestMobileConnect(t, tc.responses)
|
||||
|
||||
got, err := mcc.ChargedEnergy()
|
||||
if (err != nil) != tc.wantErr {
|
||||
t.Errorf("MobileConnect.ChargedEnergy() error = %v, wantErr %v", err, tc.wantErr)
|
||||
return
|
||||
}
|
||||
if got != tc.want {
|
||||
t.Errorf("MobileConnect.ChargedEnergy() = %v, want %v", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMobileConnect_ChargingTime(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
responses []apiResponse
|
||||
want time.Duration
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
"valid response",
|
||||
[]apiResponse{
|
||||
{apiCurrentSession, "\"{\\n \\\"account\\\": \\\"PRIVATE\\\",\\n \\\"chargingRate\\\": 0,\\n \\\"chargingType\\\": \\\"AC\\\",\\n \\\"clockSrc\\\": \\\"NTP\\\",\\n \\\"costs\\\": 0,\\n \\\"currency\\\": \\\"\\\",\\n \\\"departTime\\\": \\\"\\\",\\n \\\"duration\\\": 30789,\\n \\\"endOfChargeTime\\\": \\\"\\\",\\n \\\"endSoc\\\": 0,\\n \\\"endTime\\\": \\\"\\\",\\n \\\"energySumKwh\\\": 18.832000000000001,\\n \\\"evChargingRatekW\\\": 0,\\n \\\"evTargetSoc\\\": -1,\\n \\\"evVasAvailability\\\": false,\\n \\\"pcid\\\": \\\"\\\",\\n \\\"powerRange\\\": 0,\\n \\\"selfEnergy\\\": 0,\\n \\\"sessionId\\\": 13,\\n \\\"soc\\\": -1,\\n \\\"solarEnergyShare\\\": 0,\\n \\\"startSoc\\\": 0,\\n \\\"startTime\\\": \\\"2020-04-15T10:07:22+02:00\\\",\\n \\\"totalRange\\\": 0,\\n \\\"vehicleBrand\\\": \\\"\\\",\\n \\\"vehicleModel\\\": \\\"\\\",\\n \\\"whitelist\\\": false\\n}\\n\""},
|
||||
}, 30789 * time.Second, false,
|
||||
},
|
||||
{
|
||||
"error response",
|
||||
[]apiResponse{
|
||||
{apiCurrentSession, "\"\""},
|
||||
}, 0, true,
|
||||
},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
mcc := NewTestMobileConnect(t, tc.responses)
|
||||
|
||||
got, err := mcc.ChargingTime()
|
||||
if (err != nil) != tc.wantErr {
|
||||
t.Errorf("MobileConnect.ChargingTime() error = %v, wantErr %v", err, tc.wantErr)
|
||||
return
|
||||
}
|
||||
if got != tc.want {
|
||||
t.Errorf("MobileConnect.ChargingTime() = %v, want %v", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -83,6 +83,10 @@ chargers:
|
|||
- name: go-e
|
||||
type: go-e # go-e charger
|
||||
uri: http://192.168.1.4 # go-e address
|
||||
- name: mcc
|
||||
type: mcc # Mobile Charger Connect (Audi, Bentley, Porsche)
|
||||
uri: https://192.168.1.4 # Mobile Charger Connect address
|
||||
password: # home user password
|
||||
- name: configurable
|
||||
type: default # Configurable charger
|
||||
status: # charger status A..F
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue