Zum Inhalt

hyundai_soc.tc

hyundai_soc.tc — EV state-of-charge from myHyundai/Bluelink, ENTIRELY on the ESP.

Source on GitHub

// hyundai_soc.tc — EV state-of-charge from myHyundai/Bluelink, ENTIRELY on the ESP.
// refresh token (/hytoken.cfg) → access token → vehicle list → cached status → SoC.
// SELF-HEALING: when the refresh token expires, it re-mints a fresh one on-device
// via the IDPConnect RSA login from /hyundai.cfg (mail/pw/pin) and retries — no Mac.
// All over the raw TLS + base64/stamp + rsaEncrypt primitives. Console: HYS / HYS r
// (poll) / HYS l (force re-login). Web: a "Car SOC" sensor line.

#define HOST    "prd.eu-ccapi.hyundai.com"
#define PORT    8080
#define TOKHOST "idpconnect-eu.hyundai.com"
#define APPID   "014d2225-8495-4735-812d-2616334fd15d"
#define SVCID   "6d477c38-3ca4-4cf3-9557-2a1929a94654"
#define SECRET  "KUy49XxPzLpLuoK0xhBC77W6VXhmtQR9iQhmIFjjoY4IpxsV"
#define DEVID   "590bfcdc-6f9c-4fa3-918d-205b81aefa5b"
#define RUENC   "https%3A%2F%2Fprd.eu-ccapi.hyundai.com%3A8080%2Fapi%2Fv1%2Fuser%2Foauth2%2Ftoken"
#define UA      "Mozilla/5.0 (Linux; Android 4.1.1; Galaxy Nexus Build/JRO03C) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.166 Mobile Safari/535.19_CCS_APP_AOS"
#define FORCE_COOL 300    // min seconds between manual force-refreshes (protects the 12V aux battery + respects Hyundai's throttle)

int cfb[50] = {68,91,104,70,175,239,13,114,102,70,119,104,101,166,80,201,243,168,
               183,179,171,34,161,149,22,63,122,137,141,150,47,124,178,31,150,127,
               165,75,229,82,26,166,11,16,246,183,224,250,229,36};

char stamp[80]; char rtoken[80]; char access[800]; char vid[60];
char resp[3200]; char snip[170];
int s_tok = -9; int s_veh = -9; int s_sta = -9; int soc = -1; int chg = 0;
int flow_req = 0; int relog_req = 0;
int force_req = 0; int g_force = 0; int last_force = 0;   // manual live-refresh (wakes the car): request flag, one-shot force-mode, rate-limit timestamp

// re-login state (the login reuses resp for request bodies + responses to save RAM)
char lmail[80]; char lpw[44]; char lcookie[100]; char ljn[360]; char lje[8];
char lkid[64]; char lenc[540]; char lcode[140]; char lhdr[768];
int rl_st1 = 0; int rl_st2 = 0; int rl_st4 = 0; int rl_st5 = 0; int rl_ok = 0; int rl_skip = 0;

void makeStamp() {
    int epoch = utcSecs();
    char raw[60]; sprintf(raw, "%s:%d", APPID, epoch);
    int n = strlen(raw); char xb[60]; int i;
    for (i = 0; i < n; i = i + 1) { xb[i] = (raw[i] ^ cfb[i]) & 255; }
    base64Enc(xb, n, stamp);
}

void loadToken() {
    rtoken[0] = 0;
    int f = fileOpen("/hytoken.cfg", "r");
    if (f < 0) { return; }
    int n = fileRead(f, rtoken, 70);
    fileClose(f);
    if (n < 0) { n = 0; }
    while (n > 0 && rtoken[n - 1] <= 32) { n = n - 1; }
    rtoken[n] = 0;
}

void authGet(char path[]) {
    char p1[560];
    sprintf(p1, "GET %s HTTP/1.0\r\nHost: %s:%d\r\nccsp-service-id: %s\r\nccsp-application-id: %s\r\nStamp: %s\r\nccsp-device-id: %s\r\nConnection: close\r\nUser-Agent: okhttp/3.12.1\r\n",
            path, HOST, PORT, SVCID, APPID, stamp, DEVID);
    char p2[860]; strcpy(p2, "Authorization: Bearer "); strcat(p2, access); strcat(p2, "\r\n\r\n");  // must hold "Authorization: Bearer "(22)+access[800]+"\r\n\r\n"(4); was 700 -> heap overflow on long JWT tokens
    tlsWrite(p1); tlsWrite(p2);
}

int readStatus() {                                 // status line → HTTP code, skip headers
    char sline[120]; tlsReadLine(sline);
    char code[8]; strSub(code, sline, 9, 3);
    int m = 1; while (m > 0) { m = tlsReadLine(sline); }
    return atoi(code);
}

void getToken() {
    loadToken();
    access[0] = 0;
    if (strlen(rtoken) < 10) { s_tok = -2; return; }
    if (tlsConnect(TOKHOST, 443) != 0) { s_tok = -1; return; }
    char body[280];
    sprintf(body, "grant_type=refresh_token&refresh_token=%s&client_id=%s&client_secret=%s", rtoken, SVCID, SECRET);
    char hdr[360];
    sprintf(hdr, "POST /auth/api/v2/user/oauth2/token HTTP/1.0\r\nHost: %s\r\nContent-Type: application/x-www-form-urlencoded\r\nConnection: close\r\nContent-Length: %d\r\n\r\n", TOKHOST, strlen(body));
    tlsWrite(hdr); tlsWrite(body);
    s_tok = readStatus();
    tlsRead(resp, 1550);
    int p = strFind(resp, "access_token");
    if (p >= 0) { int vs = p + 15; int k = 0;
        while (resp[vs + k] != 0 && resp[vs + k] != 34 && k < 780) { access[k] = resp[vs + k]; k = k + 1; }
        access[k] = 0; }
    tlsStop();
}

// ───────────── on-device re-login (refresh token expired) ─────────────
void loadCreds() {
    lmail[0] = 0; lpw[0] = 0;
    int f = fileOpen("/hyundai.cfg", "r");
    if (f < 0) { return; }
    char buf[170]; int n = fileRead(f, buf, 160); fileClose(f);
    if (n < 0) { n = 0; } buf[n] = 0;
    int i = 0; int k = 0;
    while (buf[i] != 0 && buf[i] != 10 && buf[i] != 13 && k < 78) { lmail[k] = buf[i]; i = i + 1; k = k + 1; }
    lmail[k] = 0;
    while (buf[i] == 10 || buf[i] == 13) { i = i + 1; }
    k = 0;
    while (buf[i] != 0 && buf[i] != 10 && buf[i] != 13 && k < 42) { lpw[k] = buf[i]; i = i + 1; k = k + 1; }
    lpw[k] = 0;
}

int lConn() {                                      // connect TOKHOST, retry on fragmented heap
    int t = 0;
    while (t < 5) { if (tlsConnect(TOKHOST, 443) == 0) { return 0; } t = t + 1; delay(1500); }
    return -1;
}
int lStat() { char ln[200]; tlsReadLine(ln); char c[8]; strSub(c, ln, 9, 3); return atoi(c); }
void lSkip() { int m = 1; while (m > 0) { m = tlsReadLine(lhdr); } }
void lCap() {                                      // read headers; grab account cookie + ?code=
    while (1) {
        int n = tlsReadLine(lhdr);
        if (n <= 0) { break; }
        if (strFind(lhdr, "Set-Cookie: account=") == 0) {
            int vs = 20; int k = 0;
            while (lhdr[vs + k] != 0 && lhdr[vs + k] != 59 && k < 95) { lcookie[k] = lhdr[vs + k]; k = k + 1; }
            lcookie[k] = 0;
        }
        int lp = strFind(lhdr, "code=");
        if (lp >= 0 && lcode[0] == 0) {
            int vs = lp + 5; int k = 0;
            while (lhdr[vs + k] != 0 && lhdr[vs + k] != 38 && lhdr[vs + k] != 32 && k < 130) { lcode[k] = lhdr[vs + k]; k = k + 1; }
            lcode[k] = 0;
        }
    }
}

void rlStep1() {                                   // GET authorize → account cookie
    lcookie[0] = 0;
    if (lConn() != 0) { rl_st1 = -1; return; }
    sprintf(lhdr, "GET /auth/api/v2/user/oauth2/authorize?response_type=code&client_id=%s&redirect_uri=%s&lang=en&state=ccsp&country=de HTTP/1.0\r\nHost: %s\r\nUser-Agent: %s\r\nConnection: close\r\n\r\n",
            SVCID, RUENC, TOKHOST, UA);
    tlsWrite(lhdr);
    rl_st1 = lStat(); lCap(); tlsStop();
}

void rlStep2() {                                   // GET certs → JWK n,e,kid
    ljn[0] = 0; lje[0] = 0; lkid[0] = 0;
    if (lConn() != 0) { rl_st2 = -1; return; }
    sprintf(lhdr, "GET /auth/api/v1/accounts/certs HTTP/1.0\r\nHost: %s\r\nUser-Agent: %s\r\nCookie: account=%s\r\nConnection: close\r\n\r\n",
            TOKHOST, UA, lcookie);
    tlsWrite(lhdr);
    rl_st2 = lStat(); lSkip(); tlsRead(resp, 1400);
    int p = strFind(resp, "\"n\":\"");
    if (p >= 0) { int vs = p + 5; int k = 0; while (resp[vs + k] != 0 && resp[vs + k] != 34 && k < 350) { ljn[k] = resp[vs + k]; k = k + 1; } ljn[k] = 0; }
    p = strFind(resp, "\"e\":\"");
    if (p >= 0) { int vs = p + 5; int k = 0; while (resp[vs + k] != 0 && resp[vs + k] != 34 && k < 6) { lje[k] = resp[vs + k]; k = k + 1; } lje[k] = 0; }
    p = strFind(resp, "\"kid\":\"");
    if (p >= 0) { int vs = p + 7; int k = 0; while (resp[vs + k] != 0 && resp[vs + k] != 34 && k < 60) { lkid[k] = resp[vs + k]; k = k + 1; } lkid[k] = 0; }
    tlsStop();
}

void rlStep4() {                                   // POST signin → 302 ?code=
    lcode[0] = 0;
    if (lConn() != 0) { rl_st4 = -1; return; }
    sprintf(resp, "client_id=%s&encryptedPassword=true&password=", SVCID);   // body in resp
    strcat(resp, lenc);
    strcat(resp, "&redirect_uri=");
    strcat(resp, RUENC);
    strcat(resp, "&scope=&nonce=&state=ccsp&username=");
    strcat(resp, lmail);
    strcat(resp, "&connector_session_key=&kid=");
    strcat(resp, lkid);
    strcat(resp, "&_csrf=");
    int blen = strlen(resp);
    sprintf(lhdr, "POST /auth/account/signin HTTP/1.0\r\nHost: %s\r\nUser-Agent: %s\r\nCookie: account=%s\r\nContent-Type: application/x-www-form-urlencoded\r\nConnection: close\r\nContent-Length: %d\r\n\r\n",
            TOKHOST, UA, lcookie, blen);
    tlsWrite(lhdr); tlsWrite(resp);
    rl_st4 = lStat(); lCap(); tlsStop();
}

void rlStep5() {                                   // POST token → new refresh_token → save
    rl_ok = 0;
    if (lConn() != 0) { rl_st5 = -1; return; }
    sprintf(resp, "grant_type=authorization_code&code=%s&redirect_uri=%s&client_id=%s&client_secret=%s",
            lcode, RUENC, SVCID, SECRET);
    int blen = strlen(resp);
    sprintf(lhdr, "POST /auth/api/v2/user/oauth2/token HTTP/1.0\r\nHost: %s\r\nContent-Type: application/x-www-form-urlencoded\r\nConnection: close\r\nContent-Length: %d\r\n\r\n",
            TOKHOST, blen);
    tlsWrite(lhdr); tlsWrite(resp);
    rl_st5 = lStat(); lSkip(); tlsRead(resp, 1400);
    int p = strFind(resp, "refresh_token");
    if (p >= 0) {
        char nrt[60]; int vs = p + 16; int k = 0;
        while (resp[vs + k] != 0 && resp[vs + k] != 34 && k < 55) { nrt[k] = resp[vs + k]; k = k + 1; }
        nrt[k] = 0;
        int valid = (k == 48); int q = 0;
        while (valid && q < 48) { int c = nrt[q]; if (!((c >= 48 && c <= 57) || (c >= 65 && c <= 90))) { valid = 0; } q = q + 1; }
        if (valid) {
            int fo = fileOpen("/hytoken.cfg", "w");
            if (fo >= 0) { fileWrite(fo, nrt, 48); fileClose(fo); rl_ok = 1; addLog("HYS: re-login saved a fresh token"); }
        }
    }
    tlsStop();
}

void doRelogin() {
    rl_st1 = 0; rl_st2 = 0; rl_st4 = 0; rl_st5 = 0; rl_ok = 0;
    loadCreds();
    if (strlen(lmail) < 5 || strlen(lpw) < 3) { rl_st1 = -8; return; }
    rlStep1();
    if (rl_st1 == 302) { rlStep2(); }
    if (rl_st2 == 200 && strlen(ljn) > 100) { int n = rsaEncrypt(ljn, lje, lpw, lenc); if (n > 0) { rlStep4(); } }
    if (rl_st4 == 302 && strlen(lcode) > 10) { rlStep5(); }
    addLog("HYS relogin: st1=%d st2=%d st4=%d st5=%d ok=%d", rl_st1, rl_st2, rl_st4, rl_st5, rl_ok);
}

void getVehicles() {
    makeStamp();
    if (tlsConnect(HOST, PORT) != 0) { s_veh = -1; return; }
    authGet("/api/v1/spa/vehicles");
    s_veh = readStatus();
    tlsRead(resp, 2150);
    vid[0] = 0;
    int p = strFind(resp, "vehicleId");
    if (p >= 0) { int vs = p + 12; int k = 0;
        while (resp[vs + k] != 0 && resp[vs + k] != 34 && k < 55) { vid[k] = resp[vs + k]; k = k + 1; }
        vid[k] = 0; }
    tlsStop();
}

void getStatus() {
    int force = g_force; g_force = 0;                    // consume the one-shot force flag (cleared on every path)
    makeStamp();
    if (tlsConnect(HOST, PORT) != 0) { s_sta = -1; return; }
    char path[110];
    if (force) { sprintf(path, "/api/v1/spa/vehicles/%s/status", vid); }         // FORCE: wakes the car -> live SoC (12V cost, throttled)
    else       { sprintf(path, "/api/v1/spa/vehicles/%s/status/latest", vid); }  // cached: last value the car itself uploaded
    authGet(path);
    if (force) {
        // The server long-polls silently while the car wakes (~10-60s); tlsReadLine's
        // 3s idle timeout would give up first, so wait on tlsAvailable() until the
        // response actually starts, THEN read normally.
        int w = 0; while (tlsAvailable() == 0 && tlsConnected() && w < 120) { delay(500); w = w + 1; }
    }
    s_sta = readStatus();
    tlsRead(resp, 3100);
    int j; for (j = 0; j < 160; j = j + 1) { int c = resp[j]; if (c == 0) break; if (c == 34 || c == 123 || c == 125) c = 32; snip[j] = c; }
    snip[j] = 0;
    soc = -1;
    int p = strFind(resp, "batteryStatus");
    if (p >= 0) { char num[8]; strSub(num, resp, p + 15, 6); soc = atoi(num); }
    chg = (strFind(resp, "batteryCharge\":true") >= 0);
    if (soc >= 0) { shareSetInt("csoc", soc); shareSetInt("cchg", chg); }
    tlsStop();
    addLog("HYS: s_sta=%d soc=%d force=%d", s_sta, soc, force);
}

void doFlow() {
    getToken();
    // self-heal: got a response (not a connect error) but no usable access token →
    // the refresh token is bad/expired → re-mint it from creds and retry once.
    int authfail = (s_tok != -1 && strlen(access) < 20);
    if ((authfail && rl_skip == 0) || relog_req == 1) {
        relog_req = 0;
        doRelogin();
        if (rl_ok == 1) { getToken(); } else { rl_skip = 12; }   // backoff ~6h on failure
    }
    if (rl_skip > 0) { rl_skip = rl_skip - 1; }
    if (s_tok == 200) { getVehicles(); }
    if (s_veh == 200 && strlen(vid) > 8) { getStatus(); }
}

void TaskLoop() {
    int t = 0;
    while (!tasm_wifi) { delay(500); }   // never start TLS before WiFi is up — TLS-before-link corrupts the heap and crashes at boot
    delay(2000);                          // let DHCP/DNS settle after association
    doFlow();
    while (1) {
        if (force_req && tasm_wifi) { force_req = 0; g_force = 1; doFlow(); }   // manual live-refresh (wakes the car); one at a time
        else if (flow_req && tasm_wifi) { flow_req = 0; doFlow(); }             // deferred cached run until the link is back
        t = t + 1;
        if (t >= 3600) { t = 0; if (tasm_wifi) { doFlow(); } }
        delay(500);
    }
}

void WebCall() {
    char b[80];
    if (soc < 0) { webSend("{s}Car SOC (IONIQ){m}- (not read){e}"); }
    else {
        char st[12]; if (chg) { strcpy(st, " +laden"); } else { st[0] = 0; }
        sprintf(b, "{s}Car SOC (IONIQ){m}%d%%%s{e}", soc, st);
        webSend(b);
    }
    // Manual live-refresh button: wakes the car for a fresh reading (rate-limited, ~40s).
    webSend("{s}{m}<button onclick=\"fetch('/cm?cmnd=HYS%20f').then(r=>r.json()).then(j=>alert(j.TinyC))\" style='padding:4px 10px'>&#128260; Live-Abfrage (weckt Auto)</button>{e}");
}

void Command(char cmd[]) {
    int i = 0; while (cmd[i] == ' ') { i = i + 1; }
    if (cmd[i] == 'r' || cmd[i] == 'R') { flow_req = 1; responseCmnd("RUN"); return; }
    if (cmd[i] == 'l' || cmd[i] == 'L') { relog_req = 1; flow_req = 1; responseCmnd("RELOGIN queued"); return; }
    if (cmd[i] == 'f' || cmd[i] == 'F') {                          // manual live-refresh (wakes the car), rate-limited
        int now = utcSecs(); int since = now - last_force;
        if (last_force != 0 && now > 1000000000 && since < FORCE_COOL) {
            char m[52]; sprintf(m, "rate limited: wait %ds", FORCE_COOL - since);
            responseCmnd(m); return;
        }
        last_force = now; force_req = 1;
        responseCmnd("LIVE refresh queued (wakes car ~40s)"); return;
    }
    char r[240];
    sprintf(r, "s_tok=%d s_veh=%d s_sta=%d soc=%d chg=%d relog_ok=%d skip=%d snip=[%s]",
            s_tok, s_veh, s_sta, soc, chg, rl_ok, rl_skip, snip);
    responseCmnd(r);
}

int main() { addCommand("HYS"); addLog("hyundai_soc ready (self-healing token)"); return 0; }