106 lines
2.9 KiB
Go
106 lines
2.9 KiB
Go
package bluelink
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/evcc-io/evcc/util"
|
|
"github.com/evcc-io/evcc/util/request"
|
|
"github.com/evcc-io/evcc/util/transport"
|
|
)
|
|
|
|
const (
|
|
VehiclesURL = "vehicles"
|
|
StatusURL = "vehicles/%s/status" // Triggers refresh from vehicle (older API)
|
|
StatusLatestURL = "vehicles/%s/status/latest" // Cached data with location/odometer (older API)
|
|
StatusLatestURLCCS2 = "vehicles/%s/ccs2/carstatus/latest" // Newer API (2024+ vehicles with ccOS)
|
|
)
|
|
|
|
const (
|
|
resOK = "S" // auth fail: F
|
|
)
|
|
|
|
// ErrAuthFail indicates authorization failure
|
|
var ErrAuthFail = errors.New("authorization failed")
|
|
|
|
// API implements the Kia/Hyundai bluelink api.
|
|
type API struct {
|
|
*request.Helper
|
|
baseURI string
|
|
}
|
|
|
|
// New creates a new BlueLink API
|
|
func NewAPI(log *util.Logger, baseURI string, decorator func(*http.Request) error) *API {
|
|
v := &API{
|
|
Helper: request.NewHelper(log),
|
|
baseURI: strings.TrimSuffix(baseURI, "/api/v1/spa") + "/api/v1/spa",
|
|
}
|
|
|
|
// api is unbelievably slow when retrieving status
|
|
v.Client.Timeout = 120 * time.Second
|
|
|
|
if transport, ok := v.Client.Transport.(*http.Transport); ok {
|
|
transport.TLSHandshakeTimeout = 30 * time.Second
|
|
}
|
|
|
|
v.Client.Transport = &transport.Decorator{
|
|
Decorator: decorator,
|
|
Base: v.Client.Transport,
|
|
}
|
|
|
|
return v
|
|
}
|
|
|
|
type Vehicle struct {
|
|
VIN, VehicleName, VehicleID string
|
|
CcuCCS2ProtocolSupport int
|
|
}
|
|
|
|
func (v *API) Vehicles() ([]Vehicle, error) {
|
|
var res VehiclesResponse
|
|
|
|
uri := fmt.Sprintf("%s/%s", v.baseURI, VehiclesURL)
|
|
err := v.GetJSON(uri, &res)
|
|
|
|
return res.ResMsg.Vehicles, err
|
|
}
|
|
|
|
// StatusLatest retrieves vehicle status (triggers refresh for older API, then returns cached data)
|
|
func (v *API) StatusLatest(vehicle Vehicle) (BluelinkVehicleStatusLatest, error) {
|
|
vid := vehicle.VehicleID
|
|
|
|
if vehicle.CcuCCS2ProtocolSupport != 0 {
|
|
var res StatusLatestResponseCCS
|
|
uri := fmt.Sprintf("%s/%s", v.baseURI, fmt.Sprintf(StatusLatestURLCCS2, vid))
|
|
err := v.GetJSON(uri, &res)
|
|
if err == nil && res.RetCode != resOK {
|
|
err = fmt.Errorf("unexpected response: %s", res.RetCode)
|
|
}
|
|
return res, err
|
|
}
|
|
|
|
// For older API: first trigger refresh, then get latest cached data
|
|
_ = v.Refresh(vehicle) // Ignore error, will retry with /status/latest
|
|
|
|
var res StatusLatestResponse
|
|
uri := fmt.Sprintf("%s/%s", v.baseURI, fmt.Sprintf(StatusLatestURL, vid))
|
|
err := v.GetJSON(uri, &res)
|
|
if err == nil && res.RetCode != resOK {
|
|
err = fmt.Errorf("unexpected response: %s", res.RetCode)
|
|
}
|
|
return res, err
|
|
}
|
|
|
|
// Refresh triggers a status update from the vehicle
|
|
func (v *API) Refresh(vehicle Vehicle) error {
|
|
var res StatusResponse
|
|
uri := fmt.Sprintf("%s/%s", v.baseURI, fmt.Sprintf(StatusURL, vehicle.VehicleID))
|
|
err := v.GetJSON(uri, &res)
|
|
if err == nil && res.RetCode != resOK {
|
|
err = fmt.Errorf("unexpected response: %s", res.RetCode)
|
|
}
|
|
return err
|
|
}
|