Skip to content

Instantly share code, notes, and snippets.

@kolypto
Created September 26, 2023 14:37
Show Gist options
  • Select an option

  • Save kolypto/7ff7008d2267de424ccc95ac5b10fcba to your computer and use it in GitHub Desktop.

Select an option

Save kolypto/7ff7008d2267de424ccc95ac5b10fcba to your computer and use it in GitHub Desktop.

Revisions

  1. kolypto created this gist Sep 26, 2023.
    165 changes: 165 additions & 0 deletions README.md
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,165 @@
    # Starline API

    Чтобы получить доступ, нужно войти под своей учетной записью на my.starline.ru и перейти на страницу
    https://my.starline.ru/developer. После заполнения формы, заявку на предоставление доступа к API для аккаунта рассмотрят сотрудники StarLine.

    Получение кода приложения для дальнейшего получения токена. Срок годности кода приложения – 1 час.

    ```console
    $ http GET https://id.starline.ru/apiV3/application/getCode appId==123456 secret==(echo -n "<secret>" | md5sum | cut -d' ' -f1)

    {
    "desc": {
    "code": "9a28848db17ef477f4896ac4ecf360ad"
    },
    "state": 1
    }
    ```

    Получение токена приложения для дальнейшей авторизации. Время жизни токена приложения – 4 часа.

    ```console
    $ http GET https://id.starline.ru/apiV3/application/getToken appId==123456 secret==(echo -n "<secret><code>" | md5sum | cut -d' ' -f1)

    {
    "desc": {
    "token": "b8acd7d7fc95d3bfe1e4228f6374738fb9591fd79fb1937d7258916935463276"
    },
    "state": 1
    }
    ```

    Получение user_token:

    ```console
    $ http --form POST https://id.starline.ru/apiV3/user/login Token:b8acd7d7fc95d3bfe1e4228f6374738fb9591fd79fb1937d7258916935463276 login=user@example.com pass=(echo -n "<password>" | sha1sum | cut -d' ' -f1)

    {
    "desc": {
    "_check_password_strength": 1,
    "auth_contact_id": null,
    "avatar": "default",
    "company_name": "",
    "date_register": "2015-02-11 19:17:22",
    "first_name": "John",
    "gmt": "+3",
    "id": "111222",
    "lang": "ru",
    "last_auth_date": "2023-09-22 23:01:09",
    "last_auth_ip": "109.168.229.180",
    "last_name": "Smith",
    "login": "UserName",
    "middle_name": "",
    "roles": [
    "user",
    "open-api-user"
    ],
    "sex": "M",
    "state": "ACTIVE",
    "subscription": null,
    "user_token": "11b1b9600d18d3e2c26777aea56ddd16:238311"
    },
    "state": 1
    }
    ```


    Полученный в результате успешного выполнения команды cookie необходимо использовать в методах WebAPI.
    Данный токен действителен 24 часа.

    ```console
    $ http POST https://developer.starline.ru/json/v2/auth.slid slid_token=11b1b9600d18d3e2c26777aea56ddd16:111222
    Set-Cookie: slnet=BA3D4AB891E5E576A12C57B0812D4F99;
    {
    "code": "200",
    "codestring": "OK",
    "nchan_id": "DF0EC51FE9481C57DBE4706E0BB25ED8",
    "realplexor_id": "DF0EC51FE9481C57DBE4706E0BB25ED8",
    "user_id": "111222"
    }
    ```

    Теперь можно получать информацию о машине:

    ```console
    $ http GET https://developer.starline.ru/json/v1/user/<user_id>/user_info Cookie:slnet=BA3D4AB891E5E576A12C57B0812D4F99
    {
    "code": 200,
    "codestring": "OK",
    "devices": [
    {
    "alias": "Lada Kalina",
    "balance": 865,
    "battery": 12.73,
    "car_alr_state": {
    "add_h": null,
    "add_l": null,
    "door": 2,
    "hbrake": null,
    "hijack": null,
    "hood": 2,
    "ign": 2,
    "pbrake": null,
    "shock_h": null,
    "shock_l": null,
    "tilt": null,
    "trunk": 2
    },
    "car_state": {
    "add_sens_bpass": 0,
    "alarm": 2,
    "arm": 1,
    "door": 2,
    "dvr": 0,
    "hbrake": 2,
    "hijack": 2,
    "hood": 2,
    "ign": 2,
    "out": 2,
    "pbrake": 2,
    "r_start": 2,
    "relay": 0,
    "run": 2,
    "shock_bpass": 0,
    "tilt_bpass": 0,
    "trunk": 2,
    "valet": 2,
    "webasto": 2
    },
    "ctemp": 29,
    "device_id": "11223344",
    "diag": {
    "can_descr": "5271",
    "can_version": "4.4.0",
    "vin": ""
    },
    "etemp": 74,
    "fw_version": "FG33-P4,GK74-P7",
    "gps_lvl": 0,
    "gsm_lvl": 6,
    "hchan_channel": null,
    "imei": "112233445566",
    "mayak_temp": 865,
    "mon_type": 2,
    "phone": "+79991112233",
    "position": {
    "dir": null,
    "r": 179,
    "s": null,
    "sat_qty": null,
    "ts": 748729163,
    "x": "00.000000",
    "y": "00.000000"
    },
    "reg": null,
    "rpl_channel": null,
    "sn": null,
    "status": 2,
    "ts_activity": 748700007,
    "type": 10
    }
    ],
    "shared_devices": []
    }
    ```

    265 changes: 265 additions & 0 deletions main.go
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,265 @@
    // LICENSE: MIT
    // Author: Mark Vartanyan <kolypto@gmail.com>

    package main

    import (
    "crypto/md5"
    "crypto/sha1"
    "encoding/hex"
    "fmt"
    "strconv"
    "time"

    "github.com/cockroachdb/errors"
    "github.com/go-resty/resty/v2"
    )

    func main() {
    for {
    starline := NewStarlineClient()
    err := starline.Authenticate(12345, "<app-secret>", "user@example.com", "<password>")
    if err != nil {
    fmt.Printf("err: %+v\n", err)
    // Sleep, reconnect
    time.Sleep(10 * time.Second)
    }

    for {
    info, err := starline.GetUserInfo()
    if errors.Is(err, ErrUnauthorized) {
    fmt.Printf("err: Unauthorized. Need to sign in again\n")
    fmt.Printf("err: %v\n", err)
    return
    }
    if err != nil {
    fmt.Printf("err: %+v\n", err)
    break
    }

    fmt.Printf("info: %+v\n", info)

    isCarRunning := info.Devices[0].CarState.Ignition == 0
    fmt.Printf("isCarRunning: %v\n", isCarRunning)

    // {Ignition:2}: parked
    // Give it a break
    time.Sleep(60 * time.Second)
    }

    }
    }

    func NewStarlineClient() *StarlineClient {
    return &StarlineClient{
    client: resty.New(),
    }
    }

    type StarlineClient struct {
    client *resty.Client
    // User id from authentication
    userId string
    }

    func (s *StarlineClient) Authenticate(appId int, appSecret string, login string, password string) error {

    // Получение кода приложения для дальнейшего получения токена. Срок годности кода приложения – 1 час.
    // $ http GET https://id.starline.ru/apiV3/application/getCode \
    // appId==123456 secret==(echo -n "SeCrEt" | md5sum | cut -d' ' -f1)
    // { "desc": { "code": "9a28848db17ef477f4896ac4ecf360ad" }, "state": 1 }
    appSecretMd5Bytes := md5.Sum([]byte(appSecret))
    appSecretMd5 := hex.EncodeToString(appSecretMd5Bytes[:])
    var getCodeResult struct {
    // Result: 0 failure, 1 ok
    State int `json:"state"`
    Desc struct {
    // Success
    Code string `json:"code"`
    // Failure
    Message string `json:"message"`
    } `json:"desc"`
    }
    res, err := s.client.R().
    SetResult(&getCodeResult).
    SetQueryParam("appId", strconv.Itoa(appId)).
    SetQueryParam("secret", appSecretMd5).
    Get("https://id.starline.ru/apiV3/application/getCode")
    if DEBUG {
    fmt.Printf("getCode:\n")
    fmt.Printf(" res.Request.URL: %v\n", res.Request.URL)
    fmt.Printf(" getCodeResult: %+v\n", getCodeResult)
    fmt.Printf(" res: %v\n", res)
    }
    if err != nil {
    return errors.Wrap(err, "failed to getCode")
    }
    if getCodeResult.State != 1 {
    return errors.Errorf("failed to getCode: %d %s", getCodeResult.State, getCodeResult.Desc.Message)
    }

    // Получение токена приложения для дальнейшей авторизации. Время жизни токена приложения – 4 часа.
    // $ http GET https://id.starline.ru/apiV3/application/getToken \
    // appId==123456 secret==(echo -n "<secret><code>" | md5sum | cut -d' ' -f1)
    // { "desc": { "token": "b8acd7d7fc95d3bfe1e4228f6374738fb9591fd79fb1937d7258916935463276" }, "state": 1 }
    secretWithCodeBytes := md5.Sum([]byte(appSecret + getCodeResult.Desc.Code))
    secretWithCode := hex.EncodeToString(secretWithCodeBytes[:])
    var getTokenResult struct {
    // Result: 0 failure, 1 ok
    State int `json:"state"`
    Desc struct {
    // Success
    Token string `json:"token"`
    // Failure
    Message string `json:"message"`
    } `json:"desc"`
    }
    res, err = s.client.R().
    SetResult(&getTokenResult).
    SetQueryParam("appId", strconv.Itoa(appId)).
    SetQueryParam("secret", secretWithCode).
    Get("https://id.starline.ru/apiV3/application/getToken")
    if DEBUG {
    fmt.Printf("getToken:\n")
    fmt.Printf(" res.Request.URL: %v\n", res.Request.URL)
    fmt.Printf(" getTokenResult: %+v\n", getTokenResult)
    fmt.Printf(" res: %v\n", res)
    }
    if err != nil {
    return errors.Wrap(err, "failed to getToken")
    }
    if getTokenResult.State != 1 {
    return errors.Errorf("failed to getToken: %d %s", getTokenResult.State, getTokenResult.Desc.Message)
    }

    // Получение user_token
    // $ http --form POST https://id.starline.ru/apiV3/user/login \
    // Token:b8acd7d7fc95d3bfe1e4228f6374738fb9591fd79fb1937d7258916935463276 \
    // login=user@example.com pass=(echo -n "PaSsWoRd" | sha1sum | cut -d' ' -f1)
    // { "desc": { "user_token": "11b1b9600d18d3e2c26777aea56ddd16:238311" }, "state": 1 }
    passwordSha1Bytes := sha1.Sum([]byte(password))
    passwordSha1 := hex.EncodeToString(passwordSha1Bytes[:])
    var loginResult struct {
    // Result: 0 failure, 1 ok
    State int `json:"state"`
    Desc struct {
    // Success
    UserToken string `json:"user_token"`
    // Failure
    Message string `json:"message"`
    } `json:"desc"`
    }
    res, err = s.client.R().
    SetResult(&loginResult).
    SetHeader("Token", getTokenResult.Desc.Token).
    SetFormData(map[string]string{
    "login": login,
    "pass": passwordSha1,
    }).
    Post("https://id.starline.ru/apiV3/user/login")
    if DEBUG {
    fmt.Printf("login:\n")
    fmt.Printf(" res.Request.URL: %v\n", res.Request.URL)
    fmt.Printf(" loginResult: %v\n", loginResult)
    fmt.Printf(" res: %v\n", res)
    }
    if err != nil {
    return errors.Wrap(err, "failed to login")
    }
    if loginResult.State != 1 {
    return errors.Errorf("failed to login: %d %s", loginResult.State, loginResult.Desc.Message)
    }

    // Полученный в результате успешного выполнения команды cookie необходимо использовать в методах WebAPI.
    // Данный токен действителен 24 часа.
    // $ http POST https://developer.starline.ru/json/v2/auth.slid \
    // slid_token=11b1b9600d18d3e2c26777aea56ddd16:238311
    // Set-Cookie: slnet=BA3D4AB891E5E576A12C57B0812D4F99;
    // { "code": "200", "codestring": "OK" }
    var authSlidResult struct {
    // "200" ok
    Code string `json:"code"`
    CodeString string `json:"codestring"`

    // Use in URL
    UserId string `json:"user_id"`
    }
    res, err = s.client.R().
    SetResult(&authSlidResult).
    SetBody(struct {
    SlidToken string `json:"slid_token"`
    }{
    SlidToken: loginResult.Desc.UserToken,
    }).
    Post("https://developer.starline.ru/json/v2/auth.slid")
    if DEBUG {
    fmt.Printf("auth.slid:\n")
    fmt.Printf(" res.Request.URL: %v\n", res.Request.URL)
    fmt.Printf(" authSlidResult: %v\n", authSlidResult)
    fmt.Printf(" res: %v\n", res)
    fmt.Printf(" res.Cookies(): %v\n", res.Cookies())
    }
    if err != nil {
    return errors.Wrap(err, "failed to auth.slid")
    }
    if authSlidResult.Code != "200" {
    return errors.Errorf("failed to getToken: %d %s", getTokenResult.State, getTokenResult.Desc.Message)
    }
    s.userId = authSlidResult.UserId

    return nil
    }

    // Get info about cars
    func (s *StarlineClient) GetUserInfo() (result UserInfo, err error) {
    // Теперь можно получать информацию о машине:
    // $ http GET https://developer.starline.ru/json/v1/user/<user_id>/user_info \
    // Cookie:slnet=BA3D4AB891E5E576A12C57B0812D4F99
    res, err := s.client.R().
    SetResult(&result).
    Get("https://developer.starline.ru/json/v1/user/" + s.userId + "/user_info")
    if DEBUG {
    fmt.Printf("user_info:\n")
    fmt.Printf(" res.Request.URL: %v\n", res.Request.URL)
    fmt.Printf(" result: %+v\n", result)
    fmt.Printf(" res: %v\n", res)
    fmt.Printf(" res.Request.Cookies: %v\n", res.Request.Cookies)
    }
    if result.Code == 403 {
    err = errors.CombineErrors(ErrUnauthorized, err)
    return
    }
    if result.Code != 200 {
    err = errors.Errorf("failed to get user info: %d %s", result.Code, result.CodeString)
    return
    }
    return
    }

    // Unauthorized. Need to sign in.
    var ErrUnauthorized = errors.New("Unauthorized")

    type UserInfo struct {
    Code int `json:"code"`
    CodeString string `json:"codestring"`

    // Cars
    Devices []StarlineDevice
    }

    type StarlineDevice struct {
    Alias string `json:"alias"`
    CarState struct {
    // Ignition running?
    // 2 = off, 1 = engine running
    Ignition int `json:"ign"`
    } `json:"car_state"`
    }

    // Is the engine running?
    func (d StarlineDevice) IsEngineRunning() bool {
    return d.CarState.Ignition == 1
    }

    const DEBUG = false