Add range and charge status (#842)

This commit is contained in:
Markus Thierolf 2021-04-07 20:47:22 +02:00 • committed by andig
parent 0a825267d4
commit 099f37ee4f
3 changed files with 143 additions and 29 deletions

View file

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

View file

@ -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=<NiuUser>&app_id=niu_8xt1afu6&grant_type=password&password=<MD5PasswordHash>&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=<ScooterSerialNumber>
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"`
}
}

View file

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