From 099f37ee4f4436c2f1d934a9fb32a1bfd8bdfdf5 Mon Sep 17 00:00:00 2001 From: Markus Thierolf <77847348+thierolm@users.noreply.github.com> Date: Wed, 7 Apr 2021 20:47:22 +0200 Subject: [PATCH] Add range and charge status (#842) --- internal/vehicle/niu.go | 72 ++++++++++++++++++++++-------- internal/vehicle/niu/types.go | 48 +++++++++++++++----- internal/vehicle/niu/types_test.go | 52 +++++++++++++++++++++ 3 files changed, 143 insertions(+), 29 deletions(-) create mode 100644 internal/vehicle/niu/types_test.go diff --git a/internal/vehicle/niu.go b/internal/vehicle/niu.go index 49ec3a52f..183bbfb98 100644 --- a/internal/vehicle/niu.go +++ b/internal/vehicle/niu.go @@ -20,11 +20,10 @@ import ( type Niu struct { *embed *request.Helper - user, password string - serial string - tokens niu.Token - accessTokenExpiry time.Time - chargeStateG func() (float64, error) + user, password string + serial string + token niu.Token + apiG func() (interface{}, error) } func init() { @@ -60,7 +59,7 @@ func NewNiuFromConfig(other map[string]interface{}) (api.Vehicle, error) { serial: strings.ToUpper(cc.Serial), } - v.chargeStateG = provider.NewCached(v.chargeState, cc.Cache).FloatGetter() + v.apiG = provider.NewCached(v.batteryAPI, cc.Cache).InterfaceGetter() return v, nil } @@ -86,10 +85,9 @@ func (v *Niu) login() error { }) if err == nil { - var tokens niu.Token - if err = v.DoJSON(req, &tokens); err == nil { - v.tokens = tokens - v.accessTokenExpiry = time.Unix(v.tokens.Data.Token.TokenExpiresIn, 0) + var token niu.Token + if err = v.DoJSON(req, &token); err == nil { + v.token = token } } @@ -105,32 +103,70 @@ func md5Hash(text string) (string, error) { // request implements the Niu web request func (v *Niu) request(uri string) (*http.Request, error) { - if v.tokens.Data.Token.AccessToken == "" || v.accessTokenExpiry.Before(time.Now()) { + if v.token.AccessToken == "" || time.Until(v.token.Expiry) < time.Minute { if err := v.login(); err != nil { return nil, err } } req, err := request.New(http.MethodGet, uri, nil, map[string]string{ - "token": v.tokens.Data.Token.AccessToken, + "token": v.token.AccessToken, }) return req, err } -// chargeState implements the api.Vehicle interface -func (v *Niu) chargeState() (float64, error) { - var resp niu.SoC +// batteryAPI provides battery api response +func (v *Niu) batteryAPI() (interface{}, error) { + var res niu.Response req, err := v.request(niu.ApiURI + "/v3/motor_data/index_info?sn=" + v.serial) if err == nil { - err = v.DoJSON(req, &resp) + err = v.DoJSON(req, &res) } - return float64(resp.Data.Batteries.CompartmentA.BatteryCharging), err + return res, err } // SoC implements the api.Vehicle interface func (v *Niu) SoC() (float64, error) { - return v.chargeStateG() + res, err := v.apiG() + + if res, ok := res.(niu.Response); err == nil && ok { + return float64(res.Data.Batteries.CompartmentA.BatteryCharging), nil + } + + return 0, err +} + +var _ api.ChargeState = (*Niu)(nil) + +// Status implements the api.ChargeState interface +func (v *Niu) Status() (api.ChargeStatus, error) { + status := api.StatusA // disconnected + + res, err := v.apiG() + if res, ok := res.(niu.Response); err == nil && ok { + if res.Data.IsConnected { + status = api.StatusB + } + if res.Data.IsCharging > 0 { + status = api.StatusC + } + } + + return status, err +} + +var _ api.VehicleRange = (*Niu)(nil) + +// Range implements the api.VehicleRange interface +func (v *Niu) Range() (int64, error) { + res, err := v.apiG() + + if res, ok := res.(niu.Response); err == nil && ok { + return int64(res.Data.EstimatedMileage), nil + } + + return 0, err } diff --git a/internal/vehicle/niu/types.go b/internal/vehicle/niu/types.go index 300d263a7..fbe555329 100644 --- a/internal/vehicle/niu/types.go +++ b/internal/vehicle/niu/types.go @@ -1,31 +1,57 @@ package niu +import ( + "encoding/json" + "time" + + "golang.org/x/oauth2" +) + const ( AuthURI = "https://account-fk.niu.com" ApiURI = "https://app-api-fk.niu.com" ) -// Token is the Niu oauth2 api response // https://account-fk.niu.com/v3/api/oauth2/token?account=&app_id=niu_8xt1afu6&grant_type=password&password=&scope=base -type Token struct { - Data struct { - Token struct { - AccessToken string `json:"access_token,omitempty"` - RefreshToken string `json:"refresh_token,omitempty"` - RefreshTokenExpiresIn int64 `json:"refresh_token_expires_in,omitempty"` - TokenExpiresIn int64 `json:"token_expires_in,omitempty"` +type Token oauth2.Token + +// UnmarshalJSON decodes the token api response +func (t *Token) UnmarshalJSON(data []byte) error { + var res struct { + Data struct { + Token struct { + oauth2.Token + RefreshTokenExpiresIn int64 `json:"refresh_token_expires_in,omitempty"` + TokenExpiresIn int64 `json:"token_expires_in,omitempty"` + } } } + + err := json.Unmarshal(data, &res) + if err == nil { + (*t) = (Token)(res.Data.Token.Token) + + if res.Data.Token.Expiry.IsZero() && res.Data.Token.TokenExpiresIn != 0 { + t.Expiry = time.Unix(res.Data.Token.TokenExpiresIn, 0) + } + } + + return err } -// SoC is the Niu motor_data api response +// Response is the Niu motor_data api response // https://app-api-fk.niu.com/v3/motor_data/index_info?sn= -type SoC struct { +type Response struct { Data struct { - Batteries struct { + IsCharging int `json:"isCharging,omitempty"` + IsConnected bool `json:"isConnected,omitempty"` + Timestamp int64 `json:"time,omitempty"` + Batteries struct { CompartmentA struct { BatteryCharging int64 `json:"batteryCharging,omitempty"` } `json:"compartmentA"` } + LeftTime string `json:"leftTime,omitempty"` + EstimatedMileage int64 `json:"estimatedMileage,omitempty"` } } diff --git a/internal/vehicle/niu/types_test.go b/internal/vehicle/niu/types_test.go new file mode 100644 index 000000000..53f4888e6 --- /dev/null +++ b/internal/vehicle/niu/types_test.go @@ -0,0 +1,52 @@ +package niu + +import ( + "encoding/json" + "testing" +) + +func TestUnmarshalNiuToken(t *testing.T) { + var tok Token + str := `{"data":{"token":{"access_token":"access","refresh_token":"refresh","refresh_token_expires_in":1620139071,"token_expires_in":1620139071},"user":{"user_id":"user","mobile":"","email":"email","country_code":"49","nick_name":"nickname","real_name":"realname","last_name":"lastname","identification_code":"identification_code","birthdate":"","gender":1,"avatar":"","thumb_avatar":"","profession":0,"income":0,"car_owners":0,"purpose":"","sign":"A NIU WAY FORWARD","background":"https://download.niucache.com/static/upload/20200507/57f16705-48e9-4080-8246-b2fde8df30b5.jpg","height":0,"weight":0}},"desc":"ok","status":0}` + + if err := json.Unmarshal([]byte(str), &tok); err != nil { + t.Error(err) + } + + if tok.AccessToken != "access" { + t.Error("AccessToken") + } + + if tok.RefreshToken != "refresh" { + t.Error("RefreshToken") + } + + if tok.Expiry.IsZero() { + t.Error("TokenExpiresIn") + } +} + +func TestUnmarshalNiuResponse(t *testing.T) { + var tok Response + str := `{"data":{"isCharging":0,"lockStatus":0,"isAccOn":0,"isFortificationOn":0,"isConnected":true,"postion":{"lat":42.330353,"lng":11.450598},"hdop":1,"time":1617697890173,"batteries":{"compartmentA":{"bmsId":"bmsId","isConnected":true,"batteryCharging":59,"gradeBattery":"94.8"},"compartmentB":{"bmsId":"bmsId","isConnected":true,"batteryCharging":58,"gradeBattery":"95.9"}},"leftTime":"10.2","estimatedMileage":68,"gpsTimestamp":1617697890173,"infoTimestamp":1617697890173,"nowSpeed":0,"shakingValue":3,"locationType":1,"batteryDetail":true,"centreCtrlBattery":100,"ss_protocol_ver":3,"ss_online_sta":"1","gps":5,"gsm":14,"lastTrack":{"ridingTime":90,"distance":571,"time":1617269526360}},"desc":"成功","trace":"成功","status":0}` + + if err := json.Unmarshal([]byte(str), &tok); err != nil { + t.Error(err) + } + + if tok.Data.Batteries.CompartmentA.BatteryCharging != 59 { + t.Error("BatteryCharging") + } + + if tok.Data.EstimatedMileage != 68 { + t.Error("EstimatedMileage") + } + + if tok.Data.IsCharging != 0 { + t.Error("IsCharging") + } + + if tok.Data.LeftTime != "10.2" { + t.Error("LeftTime") + } +}