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 }