Add Hyundai Bluelink (United States) (#26047)
This commit is contained in:
parent
1506de71c8
commit
14b4f7fe3e
6 changed files with 677 additions and 0 deletions
10
templates/definition/vehicle/hyundai-us.yaml
Normal file
10
templates/definition/vehicle/hyundai-us.yaml
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
template: hyundai-us
|
||||
products:
|
||||
- brand: Hyundai (US)
|
||||
description:
|
||||
generic: Bluelink
|
||||
params:
|
||||
- preset: vehicle-base
|
||||
render: |
|
||||
type: hyundai-us
|
||||
{{ include "vehicle-base" . }}
|
||||
90
vehicle/bluelink_us.go
Normal file
90
vehicle/bluelink_us.go
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
package vehicle
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/evcc-io/evcc/api"
|
||||
"github.com/evcc-io/evcc/util"
|
||||
"github.com/evcc-io/evcc/vehicle/bluelink_us"
|
||||
)
|
||||
|
||||
type BluelinkUS struct {
|
||||
*embed
|
||||
*bluelink_us.Provider
|
||||
}
|
||||
|
||||
func init() {
|
||||
registry.Add("hyundai-us", NewHyundaiUSFromConfig)
|
||||
}
|
||||
|
||||
func NewHyundaiUSFromConfig(other map[string]any) (api.Vehicle, error) {
|
||||
cc := struct {
|
||||
embed `mapstructure:",squash"`
|
||||
User, Password string
|
||||
Pin string
|
||||
VIN string
|
||||
Cache time.Duration
|
||||
}{
|
||||
Cache: interval,
|
||||
}
|
||||
|
||||
if err := util.DecodeOther(other, &cc); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if cc.User == "" || cc.Password == "" {
|
||||
return nil, api.ErrMissingCredentials
|
||||
}
|
||||
if cc.Pin == "" {
|
||||
return nil, errors.New("PIN is required")
|
||||
}
|
||||
|
||||
log := util.NewLogger("hyundai-us").Redact(cc.User, cc.Password, cc.VIN, cc.Pin)
|
||||
|
||||
identity := bluelink_us.NewIdentity(log, cc.User, cc.Password)
|
||||
if err := identity.Login(); err != nil {
|
||||
return nil, fmt.Errorf("login failed: %w", err)
|
||||
}
|
||||
|
||||
// Create temporary API to fetch vehicles (without vehicle-specific headers)
|
||||
tempCfg := bluelink_us.APIConfig{
|
||||
User: cc.User,
|
||||
}
|
||||
tempAPI := bluelink_us.NewAPI(log, identity, tempCfg)
|
||||
vehicles, err := tempAPI.Vehicles()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot get vehicles: %w", err)
|
||||
}
|
||||
|
||||
// Find matching vehicle by VIN
|
||||
vehicle, err := ensureVehicleEx(
|
||||
cc.VIN,
|
||||
func() ([]bluelink_us.Vehicle, error) { return vehicles, nil },
|
||||
func(v bluelink_us.Vehicle) (string, error) { return v.VIN, nil },
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Create full API with vehicle-specific headers
|
||||
apiCfg := bluelink_us.APIConfig{
|
||||
User: cc.User,
|
||||
Pin: cc.Pin,
|
||||
RegistrationID: vehicle.RegID,
|
||||
VIN: vehicle.VIN,
|
||||
Generation: vehicle.VehicleGeneration,
|
||||
}
|
||||
vehicleAPI := bluelink_us.NewAPI(log, identity, apiCfg)
|
||||
|
||||
v := &BluelinkUS{
|
||||
embed: &cc.embed,
|
||||
Provider: bluelink_us.NewProvider(vehicleAPI, cc.Cache),
|
||||
}
|
||||
|
||||
if v.Title_ == "" {
|
||||
v.Title_ = vehicle.NickName
|
||||
}
|
||||
|
||||
return v, nil
|
||||
}
|
||||
143
vehicle/bluelink_us/api.go
Normal file
143
vehicle/bluelink_us/api.go
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
package bluelink_us
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/evcc-io/evcc/util"
|
||||
"github.com/evcc-io/evcc/util/request"
|
||||
"github.com/evcc-io/evcc/util/transport"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
const (
|
||||
ApiURL = "/ac/v2/"
|
||||
EnrollmentURL = "enrollment/details/%s"
|
||||
VehicleStatusURL = "rcs/rvs/vehicleStatus"
|
||||
PositionURL = "rcs/rfc/findMyCar"
|
||||
ChargeStartURL = "evc/charge/start"
|
||||
ChargeStopURL = "evc/charge/stop"
|
||||
)
|
||||
|
||||
type APIConfig struct {
|
||||
User string
|
||||
Pin string
|
||||
RegistrationID string
|
||||
VIN string
|
||||
Generation string
|
||||
}
|
||||
|
||||
type API struct {
|
||||
*request.Helper
|
||||
baseURI string
|
||||
user string
|
||||
}
|
||||
|
||||
func NewAPI(log *util.Logger, ts oauth2.TokenSource, cfg APIConfig) *API {
|
||||
v := &API{
|
||||
Helper: request.NewHelper(log),
|
||||
baseURI: BaseURL + ApiURL,
|
||||
user: cfg.User,
|
||||
}
|
||||
|
||||
// API can be slow
|
||||
v.Client.Timeout = 30 * time.Second
|
||||
|
||||
// Add transport decorator for auth headers
|
||||
v.Client.Transport = &transport.Decorator{
|
||||
Decorator: func(req *http.Request) error {
|
||||
token, err := ts.Token()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for k, v := range BaseHeaders() {
|
||||
req.Header.Set(k, v)
|
||||
}
|
||||
req.Header.Set("username", cfg.User)
|
||||
req.Header.Set("accessToken", token.AccessToken)
|
||||
|
||||
// Vehicle-specific headers
|
||||
if cfg.Pin != "" {
|
||||
req.Header.Set("blueLinkServicePin", cfg.Pin)
|
||||
}
|
||||
if cfg.RegistrationID != "" {
|
||||
req.Header.Set("registrationId", cfg.RegistrationID)
|
||||
}
|
||||
if cfg.Generation != "" {
|
||||
req.Header.Set("gen", cfg.Generation)
|
||||
}
|
||||
if cfg.VIN != "" {
|
||||
req.Header.Set("vin", cfg.VIN)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
Base: v.Client.Transport,
|
||||
}
|
||||
|
||||
return v
|
||||
}
|
||||
|
||||
func (v *API) Vehicles() ([]Vehicle, error) {
|
||||
var res EnrollmentResponse
|
||||
|
||||
uri := v.baseURI + fmt.Sprintf(EnrollmentURL, v.user)
|
||||
err := v.GetJSON(uri, &res)
|
||||
|
||||
vehicles := make([]Vehicle, 0, len(res.EnrolledVehicleDetails))
|
||||
for _, entry := range res.EnrolledVehicleDetails {
|
||||
vehicles = append(vehicles, entry.VehicleDetails)
|
||||
}
|
||||
|
||||
return vehicles, err
|
||||
}
|
||||
|
||||
func (v *API) Status() (VehicleStatus, error) {
|
||||
var res VehicleStatusResponse
|
||||
|
||||
uri := v.baseURI + VehicleStatusURL
|
||||
err := v.GetJSON(uri, &res)
|
||||
|
||||
return res.VehicleStatus, err
|
||||
}
|
||||
|
||||
func (v *API) Position() (PositionResponse, error) {
|
||||
var res PositionResponse
|
||||
|
||||
uri := v.baseURI + PositionURL
|
||||
err := v.GetJSON(uri, &res)
|
||||
|
||||
return res, err
|
||||
}
|
||||
|
||||
func (v *API) ChargeStart() error {
|
||||
uri := v.baseURI + ChargeStartURL
|
||||
|
||||
req, err := request.New(http.MethodPost, uri, nil, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resp, err := v.Do(req)
|
||||
if err == nil {
|
||||
_ = resp.Body.Close()
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (v *API) ChargeStop() error {
|
||||
uri := v.baseURI + ChargeStopURL
|
||||
|
||||
req, err := request.New(http.MethodPost, uri, nil, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resp, err := v.Do(req)
|
||||
if err == nil {
|
||||
_ = resp.Body.Close()
|
||||
}
|
||||
return err
|
||||
}
|
||||
104
vehicle/bluelink_us/identity.go
Normal file
104
vehicle/bluelink_us/identity.go
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
package bluelink_us
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/evcc-io/evcc/api"
|
||||
"github.com/evcc-io/evcc/util"
|
||||
"github.com/evcc-io/evcc/util/oauth"
|
||||
"github.com/evcc-io/evcc/util/request"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
const (
|
||||
BaseURL = "https://api.telematics.hyundaiusa.com"
|
||||
TokenURL = "/v2/ac/oauth/token"
|
||||
ClientID = "m66129Bb-em93-SPAHYN-bZ91-am4540zp19920"
|
||||
ClientSecret = "v558o935-6nne-423i-baa8"
|
||||
)
|
||||
|
||||
// BaseHeaders returns the common headers required for all API requests
|
||||
func BaseHeaders() map[string]string {
|
||||
// Calculate UTC offset
|
||||
_, offset := time.Now().Zone()
|
||||
offsetHours := fmt.Sprintf("%d", offset/3600)
|
||||
|
||||
return map[string]string{
|
||||
"content-type": "application/json;charset=UTF-8",
|
||||
"accept": "application/json, text/plain, */*",
|
||||
"from": "SPA",
|
||||
"to": "ISS",
|
||||
"language": "0",
|
||||
"offset": offsetHours,
|
||||
"brandIndicator": "H",
|
||||
"client_id": ClientID,
|
||||
"clientSecret": ClientSecret,
|
||||
"refresh": "false",
|
||||
}
|
||||
}
|
||||
|
||||
type Identity struct {
|
||||
*request.Helper
|
||||
user, password string
|
||||
oauth2.TokenSource
|
||||
}
|
||||
|
||||
func NewIdentity(log *util.Logger, user, password string) *Identity {
|
||||
return &Identity{
|
||||
Helper: request.NewHelper(log),
|
||||
user: user,
|
||||
password: password,
|
||||
}
|
||||
}
|
||||
|
||||
func (v *Identity) Login() error {
|
||||
if v.user == "" || v.password == "" {
|
||||
return api.ErrMissingCredentials
|
||||
}
|
||||
|
||||
token, err := v.login()
|
||||
if err != nil {
|
||||
return fmt.Errorf("login failed: %w", err)
|
||||
}
|
||||
|
||||
v.TokenSource = oauth.RefreshTokenSource(token, v)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *Identity) login() (*oauth2.Token, error) {
|
||||
data := LoginRequest{
|
||||
Username: v.user,
|
||||
Password: v.password,
|
||||
}
|
||||
|
||||
req, err := request.New(http.MethodPost, BaseURL+TokenURL, request.MarshalJSON(data), BaseHeaders())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var res TokenResponse
|
||||
if err := v.DoJSON(req, &res); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Parse expires_in from string
|
||||
expiresIn, _ := strconv.Atoi(res.ExpiresIn)
|
||||
if expiresIn == 0 {
|
||||
expiresIn = 3600 // default to 1 hour
|
||||
}
|
||||
|
||||
return &oauth2.Token{
|
||||
AccessToken: res.AccessToken,
|
||||
RefreshToken: res.RefreshToken,
|
||||
Expiry: time.Now().Add(time.Duration(expiresIn) * time.Second),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// RefreshToken implements oauth.TokenRefresher
|
||||
// US API doesn't have a refresh endpoint, so we re-login with credentials
|
||||
func (v *Identity) RefreshToken(_ *oauth2.Token) (*oauth2.Token, error) {
|
||||
return v.login()
|
||||
}
|
||||
120
vehicle/bluelink_us/provider.go
Normal file
120
vehicle/bluelink_us/provider.go
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
package bluelink_us
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/evcc-io/evcc/api"
|
||||
"github.com/evcc-io/evcc/util"
|
||||
)
|
||||
|
||||
type Provider struct {
|
||||
statusG func() (VehicleStatus, error)
|
||||
positionG func() (PositionResponse, error)
|
||||
chargeS func(bool) error
|
||||
wakeup func() (VehicleStatus, error)
|
||||
}
|
||||
|
||||
func NewProvider(api *API, cache time.Duration) *Provider {
|
||||
v := &Provider{
|
||||
wakeup: api.Status,
|
||||
chargeS: func(enable bool) error {
|
||||
if enable {
|
||||
return api.ChargeStart()
|
||||
}
|
||||
return api.ChargeStop()
|
||||
},
|
||||
}
|
||||
|
||||
v.statusG = util.Cached(func() (VehicleStatus, error) {
|
||||
return api.Status()
|
||||
}, cache)
|
||||
|
||||
v.positionG = util.Cached(func() (PositionResponse, error) {
|
||||
return api.Position()
|
||||
}, cache)
|
||||
|
||||
return v
|
||||
}
|
||||
|
||||
var _ api.Battery = (*Provider)(nil)
|
||||
|
||||
func (v *Provider) Soc() (float64, error) {
|
||||
res, err := v.statusG()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return res.SoC()
|
||||
}
|
||||
|
||||
var _ api.ChargeState = (*Provider)(nil)
|
||||
|
||||
func (v *Provider) Status() (api.ChargeStatus, error) {
|
||||
res, err := v.statusG()
|
||||
if err != nil {
|
||||
return api.StatusNone, err
|
||||
}
|
||||
return res.Status()
|
||||
}
|
||||
|
||||
var _ api.VehicleFinishTimer = (*Provider)(nil)
|
||||
|
||||
func (v *Provider) FinishTime() (time.Time, error) {
|
||||
res, err := v.statusG()
|
||||
if err != nil {
|
||||
return time.Time{}, err
|
||||
}
|
||||
return res.FinishTime()
|
||||
}
|
||||
|
||||
var _ api.VehicleRange = (*Provider)(nil)
|
||||
|
||||
func (v *Provider) Range() (int64, error) {
|
||||
res, err := v.statusG()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return res.Range()
|
||||
}
|
||||
|
||||
var _ api.VehicleClimater = (*Provider)(nil)
|
||||
|
||||
func (v *Provider) Climater() (bool, error) {
|
||||
res, err := v.statusG()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return res.Climater()
|
||||
}
|
||||
|
||||
var _ api.SocLimiter = (*Provider)(nil)
|
||||
|
||||
func (v *Provider) GetLimitSoc() (int64, error) {
|
||||
res, err := v.statusG()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return res.GetLimitSoc()
|
||||
}
|
||||
|
||||
var _ api.VehiclePosition = (*Provider)(nil)
|
||||
|
||||
func (v *Provider) Position() (float64, float64, error) {
|
||||
res, err := v.positionG()
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
return res.Coord.Lat, res.Coord.Lon, nil
|
||||
}
|
||||
|
||||
var _ api.ChargeController = (*Provider)(nil)
|
||||
|
||||
func (v *Provider) ChargeEnable(enable bool) error {
|
||||
return v.chargeS(enable)
|
||||
}
|
||||
|
||||
var _ api.Resurrector = (*Provider)(nil)
|
||||
|
||||
func (v *Provider) WakeUp() error {
|
||||
_, err := v.wakeup()
|
||||
return err
|
||||
}
|
||||
210
vehicle/bluelink_us/types.go
Normal file
210
vehicle/bluelink_us/types.go
Normal file
|
|
@ -0,0 +1,210 @@
|
|||
package bluelink_us
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/evcc-io/evcc/api"
|
||||
)
|
||||
|
||||
// Authentication types
|
||||
|
||||
type LoginRequest struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
type TokenResponse struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
ExpiresIn string `json:"expires_in"`
|
||||
TokenType string `json:"token_type"`
|
||||
}
|
||||
|
||||
// Vehicle types
|
||||
|
||||
type Vehicle struct {
|
||||
RegID string `json:"regid"`
|
||||
VIN string `json:"vin"`
|
||||
NickName string `json:"nickName"`
|
||||
ModelCode string `json:"modelCode"`
|
||||
VehicleGeneration string `json:"vehicleGeneration"`
|
||||
EvStatus string `json:"evStatus"` // "E" = EV, "N" = ICE
|
||||
EnrollmentStatus string `json:"enrollmentStatus"`
|
||||
EnrollmentDate string `json:"enrollmentDate"`
|
||||
}
|
||||
|
||||
type EnrollmentResponse struct {
|
||||
EnrolledVehicleDetails []struct {
|
||||
VehicleDetails Vehicle `json:"vehicleDetails"`
|
||||
} `json:"enrolledVehicleDetails"`
|
||||
}
|
||||
|
||||
// Vehicle status types
|
||||
|
||||
type VehicleStatusResponse struct {
|
||||
VehicleStatus VehicleStatus `json:"vehicleStatus"`
|
||||
}
|
||||
|
||||
type VehicleStatus struct {
|
||||
DateTime string `json:"dateTime"`
|
||||
EvStatus *EvStatus `json:"evStatus,omitempty"`
|
||||
DTE *DTE `json:"dte,omitempty"` // distance to empty
|
||||
AirCtrlOn bool `json:"airCtrlOn"`
|
||||
Defrost bool `json:"defrost"`
|
||||
Engine bool `json:"engine"`
|
||||
DoorLock bool `json:"doorLock"`
|
||||
}
|
||||
|
||||
type EvStatus struct {
|
||||
BatteryStatus float64 `json:"batteryStatus"`
|
||||
BatteryCharge bool `json:"batteryCharge"`
|
||||
BatteryPlugin int `json:"batteryPlugin"`
|
||||
ChargePortDoorOpenStatus int `json:"chargePortDoorOpenStatus"`
|
||||
BatteryStndChrgPower float64 `json:"batteryStndChrgPower,omitempty"`
|
||||
DrvDistance []DrvDistance `json:"drvDistance,omitempty"`
|
||||
RemainTime2 *RemainTime `json:"remainTime2,omitempty"`
|
||||
ReservChargeInfos *ReservChargeInfo `json:"reservChargeInfos,omitempty"`
|
||||
}
|
||||
|
||||
type DrvDistance struct {
|
||||
RangeByFuel RangeByFuel `json:"rangeByFuel"`
|
||||
}
|
||||
|
||||
type RangeByFuel struct {
|
||||
EvModeRange *RangeValue `json:"evModeRange,omitempty"`
|
||||
GasModeRange *RangeValue `json:"gasModeRange,omitempty"`
|
||||
TotalAvailableRange *RangeValue `json:"totalAvailableRange,omitempty"`
|
||||
}
|
||||
|
||||
type RangeValue struct {
|
||||
Value float64 `json:"value"`
|
||||
Unit int `json:"unit"` // 1 = km, 3 = miles
|
||||
}
|
||||
|
||||
// RemainTime contains charging time estimates
|
||||
type RemainTime struct {
|
||||
Atc TimeValue `json:"atc"` // current charging method
|
||||
Etc1 TimeValue `json:"etc1"` // fast charging
|
||||
Etc2 TimeValue `json:"etc2"` // portable charging
|
||||
Etc3 TimeValue `json:"etc3"` // station charging
|
||||
}
|
||||
|
||||
type TimeValue struct {
|
||||
Value int `json:"value"` // minutes
|
||||
Unit int `json:"unit,omitempty"`
|
||||
}
|
||||
|
||||
type ReservChargeInfo struct {
|
||||
TargetSOCList []TargetSOC `json:"targetSOClist"`
|
||||
}
|
||||
|
||||
type TargetSOC struct {
|
||||
PlugType int `json:"plugType"` // 0 = DC, 1 = AC
|
||||
TargetSOCLevel int `json:"targetSOClevel"` // charge limit percentage
|
||||
}
|
||||
|
||||
// DTE is distance to empty
|
||||
type DTE struct {
|
||||
Value int `json:"value"`
|
||||
Unit int `json:"unit"` // 1 = km, 3 = miles
|
||||
}
|
||||
|
||||
// Position types
|
||||
|
||||
type PositionResponse struct {
|
||||
Coord struct {
|
||||
Lat float64 `json:"lat"`
|
||||
Lon float64 `json:"lon"`
|
||||
Alt float64 `json:"alt,omitempty"`
|
||||
} `json:"coord"`
|
||||
Time string `json:"time,omitempty"`
|
||||
}
|
||||
|
||||
const (
|
||||
plugTypeAC = 1
|
||||
unitMiles = 3
|
||||
kmPerMile = 1.60934
|
||||
timeFormatISO = "2006-01-02T15:04:05Z"
|
||||
)
|
||||
|
||||
// Interface method implementations on VehicleStatus
|
||||
|
||||
func (v VehicleStatus) Updated() (time.Time, error) {
|
||||
if t, err := time.Parse(timeFormatISO, v.DateTime); err == nil {
|
||||
return t, nil
|
||||
}
|
||||
return time.Time{}, api.ErrNotAvailable
|
||||
}
|
||||
|
||||
func (v VehicleStatus) SoC() (float64, error) {
|
||||
if v.EvStatus != nil {
|
||||
return v.EvStatus.BatteryStatus, nil
|
||||
}
|
||||
return 0, api.ErrNotAvailable
|
||||
}
|
||||
|
||||
func (v VehicleStatus) Status() (api.ChargeStatus, error) {
|
||||
if v.EvStatus != nil {
|
||||
status := api.StatusA // disconnected
|
||||
if v.EvStatus.BatteryPlugin > 0 || v.EvStatus.ChargePortDoorOpenStatus == 1 {
|
||||
status = api.StatusB // connected, not charging
|
||||
}
|
||||
if v.EvStatus.BatteryCharge {
|
||||
status = api.StatusC // charging
|
||||
}
|
||||
return status, nil
|
||||
}
|
||||
return api.StatusNone, api.ErrNotAvailable
|
||||
}
|
||||
|
||||
func (v VehicleStatus) FinishTime() (time.Time, error) {
|
||||
if v.EvStatus != nil && v.EvStatus.RemainTime2 != nil {
|
||||
remaining := v.EvStatus.RemainTime2.Atc.Value
|
||||
if remaining > 0 {
|
||||
ts, err := v.Updated()
|
||||
if err != nil {
|
||||
ts = time.Now()
|
||||
}
|
||||
return ts.Add(time.Duration(remaining) * time.Minute), nil
|
||||
}
|
||||
}
|
||||
return time.Time{}, api.ErrNotAvailable
|
||||
}
|
||||
|
||||
func (v VehicleStatus) Range() (int64, error) {
|
||||
if v.EvStatus != nil && len(v.EvStatus.DrvDistance) > 0 {
|
||||
if evRange := v.EvStatus.DrvDistance[0].RangeByFuel.EvModeRange; evRange != nil {
|
||||
value := evRange.Value
|
||||
if evRange.Unit == unitMiles {
|
||||
value *= kmPerMile
|
||||
}
|
||||
return int64(value + 0.5), nil
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to DTE for hybrids
|
||||
if v.DTE != nil && v.DTE.Value > 0 {
|
||||
value := float64(v.DTE.Value)
|
||||
if v.DTE.Unit == unitMiles {
|
||||
value *= kmPerMile
|
||||
}
|
||||
return int64(value + 0.5), nil
|
||||
}
|
||||
|
||||
return 0, api.ErrNotAvailable
|
||||
}
|
||||
|
||||
func (v VehicleStatus) Climater() (bool, error) {
|
||||
return v.AirCtrlOn || v.Defrost, nil
|
||||
}
|
||||
|
||||
func (v VehicleStatus) GetLimitSoc() (int64, error) {
|
||||
if v.EvStatus != nil && v.EvStatus.ReservChargeInfos != nil {
|
||||
for _, target := range v.EvStatus.ReservChargeInfos.TargetSOCList {
|
||||
if target.PlugType == plugTypeAC {
|
||||
return int64(target.TargetSOCLevel), nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0, api.ErrNotAvailable
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue