UNPKG

iobroker.vw-connect

Version:
1,232 lines (1,199 loc) 253 kB
// @ts-nocheck "use strict"; /* * Created with @iobroker/create-adapter v1.17.0 */ // The adapter-core module gives you access to the core ioBroker functions // you need to create an adapter const utils = require("@iobroker/adapter-core"); const request = require("request"); const qs = require("qs"); const crypto = require("crypto"); const { Crypto } = require("@peculiar/webcrypto"); const { v4: uuidv4 } = require("uuid"); const traverse = require("traverse"); const geohash = require("ngeohash"); const { extractKeys } = require("./lib/extractKeys"); const axios = require("axios").default; const Json2iob = require("json2iob"); const mqtt = require("mqtt"); const uuid = require("uuid"); class VwWeconnect extends utils.Adapter { /** * @param {Partial<ioBroker.AdapterOptions>} [options={}] */ constructor(options) { super({ ...options, name: "vw-connect", }); this.on("ready", this.onReady.bind(this)); // this.on("objectChange", this.onObjectChange.bind(this)); this.on("stateChange", this.onStateChange.bind(this)); // this.on("message", this.onMessage.bind(this)); this.on("unload", this.onUnload.bind(this)); this.extractKeys = extractKeys; this.json2iob = new Json2iob(this); this.jar = request.jar(); this.userAgent = "iobroker v"; this.skodaUserAgent = "MySkoda/Android/8.0.0/250203003"; this.refreshTokenInterval = null; this.vwrefreshTokenInterval = null; this.updateInterval = null; this.fupdateInterval = null; this.refreshTokenTimeout = null; this.homeRegion = {}; this.homeRegionSetter = {}; this.secondAccessToken = null; this.ignoredPaths = {}; this.vinArray = []; this.etags = {}; this.hasRemoteLock = false; this.isFirstLocation = true; this.lastTripCheck = 0; this.firstStart = true; this.blockTrip = {}; this.statesArray = [ { url: "$homeregion/fs-car/bs/departuretimer/v1/$type/$country/vehicles/$vin/timer", path: "timer", element: "timer", }, { url: "$homeregion/fs-car/bs/climatisation/v1/$type/$country/vehicles/$vin/climater", path: "climater", element: "climater", }, { url: "$homeregion/fs-car/bs/cf/v1/$type/$country/vehicles/$vin/position", path: "position", element: "storedPositionResponse", element2: "position", element3: "findCarResponse", element4: "Position", }, { url: "$homeregion/fs-car/bs/tripstatistics/v1/$type/$country/vehicles/$vin/tripdata/$tripType?type=list", path: "tripdata", element: "tripDataList", }, { url: "$homeregion/fs-car/bs/vsr/v1/$type/$country/vehicles/$vin/status", path: "status", element: "StoredVehicleDataResponse", element2: "vehicleData", }, { url: "$homeregion/fs-car/bs/batterycharge/v1/$type/$country/vehicles/$vin/charger", path: "charger", element: "charger", }, { url: "$homeregion/fs-car/bs/rs/v1/$type/$country/vehicles/$vin/status", path: "remoteStandheizung", element: "statusResponse", }, { url: "$homeregion/fs-car/bs/dwap/v1/$type/$country/vehicles/$vin/history", path: "history", }, ]; } /** * Is called when databases are connected and adapter received configuration. */ async onReady() { // Initialize your adapter here this.setState("info.connection", false, true); if (!this.config.password) { this.log.warn("Please enter password"); return; } this.userAgent += this.version; // Reset the connection indicator during startup this.type = "VW"; this.country = "DE"; this.clientId = "9496332b-ea03-4091-a224-8c746b885068%40apps_vw-dilab_com"; this.xclientId = "38761134-34d0-41f3-9a73-c4be88d7d337"; this.scope = "openid%20profile%20mbb%20email%20cars%20birthdate%20badge%20address%20vin"; this.redirect = "carnet%3A%2F%2Fidentity-kit%2Flogin"; this.xrequest = "de.volkswagen.carnet.eu.eremote"; this.responseType = "id_token%20token%20code"; this.xappversion = "5.1.2"; this.xappname = "eRemote"; if (this.config.type === "vw") { this.log.info("WeConnect App is disabled switch to ID/Volkswagen App"); this.config.type = "id"; } if (this.config.type === "skoda") { this.type = "Skoda"; this.country = "CZ"; this.clientId = "f9a2359a-b776-46d9-bd0c-db1904343117@apps_vw-dilab_com"; this.xclientId = "afb0473b-6d82-42b8-bfea-cead338c46ef"; this.scope = "openid mbb profile"; this.redirect = "skodaconnect://oidc.login/"; this.xrequest = "cz.skodaauto.connect"; this.responseType = "code%20id_token"; this.xappversion = "3.2.6"; this.xappname = "cz.skodaauto.connect"; } if (this.config.type === "skodae") { this.type = "Skoda"; this.country = "CZ"; this.clientId = "7f045eee-7003-4379-9968-9355ed2adb06@apps_vw-dilab_com"; this.xclientId = "afb0473b-6d82-42b8-bfea-cead338c46ef"; this.scope = "address badge birthdate cars driversLicense dealers email mileage mbb nationalIdentifier openid phone profession profile vin"; this.redirect = "myskoda%3A%2F%2Fredirect%2Flogin%2F"; this.xrequest = "cz.skodaauto.connect"; this.responseType = "code id_token"; this.xappversion = "8.0.0"; this.xappname = "cz.skodaauto.connect"; this.xbrand = "skoda"; } if (this.config.type === "seat") { this.type = "Seat"; this.country = "ES"; this.clientId = "3c8e98bc-3ae9-4277-a563-d5ee65ddebba@apps_vw-dilab_com"; this.xclientId = "9dcc70f0-8e79-423a-a3fa-4065d99088b4"; this.scope = "openid profile address phone email birthdate nationalIdentifier cars mbb dealers badge nationality"; this.redirect = "seatconnect://identity-kit/login"; // this.xrequest = "cz.skodaauto.connect"; this.responseType = "code%20id_token%20token"; this.xappversion = "1.11.2"; this.xappname = "SEAT Connect"; } if (this.config.type === "seatcupra") { this.type = "Seat"; // this.clientId = "3c756d46-f1ba-4d78-9f9a-cff0d5292d51@apps_vw-dilab_com"; this.clientId = "99a5b77d-bd88-4d53-b4e5-a539c60694a3@apps_vw-dilab_com"; this.scope = "openid profile nickname birthdate phone"; // this.redirect = "cupra://oauth-callback"; this.redirect = "seat://oauth-callback"; this.responseType = "code"; this.xappversion = "1.1.29"; this.xappname = "SEATConnect"; } if (this.config.type === "seatcupra2") { this.type = "Seat"; this.clientId = "30e33736-c537-4c72-ab60-74a7b92cfe83@apps_vw-dilab_com"; this.scope = "openid profile address phone email birthdate nationalIdentifier cars mbb dealers badge nationality"; this.redirect = "cupraconnect://identity-kit/login"; this.responseType = "code id_token token"; this.xappversion = "1.1.29"; this.xclientId = "9d183b70-d129-424f-9a26-c3778edf95e1"; this.xappname = "SEATConnect"; } if (this.config.type === "vwv2") { this.log.info("WeConnect App is disabled switch to ID/Volkswagen App"); this.config.type = "id"; this.type = "VW"; this.country = "DE"; this.clientId = "9496332b-ea03-4091-a224-8c746b885068@apps_vw-dilab_com"; this.xclientId = "89312f5d-b853-4965-a471-b0859ee468af"; this.scope = "openid profile mbb cars birthdate nickname address phone"; this.redirect = "carnet://identity-kit/login"; this.xrequest = "de.volkswagen.car-net.eu.e-remote"; this.responseType = "id_token%20token%20code"; this.xappversion = "5.6.7"; this.xappname = "We Connect"; } if (this.config.type === "id") { this.type = "Id"; this.country = "DE"; this.clientId = "a24fba63-34b3-4d43-b181-942111e6bda8@apps_vw-dilab_com"; this.xclientId = ""; this.scope = "openid profile badge cars dealers birthdate vin"; this.redirect = "weconnect://authenticated"; this.xrequest = "com.volkswagen.weconnect"; this.responseType = "code id_token token"; this.xappversion = ""; this.xappname = ""; this.xbrand = "volkswagen"; } if (this.config.type === "audi") { this.log.info("Login in with audi as audietron"); this.config.type = "audietron"; // this.type = "Audi"; // this.country = "DE"; // this.clientId = "09b6cbec-cd19-4589-82fd-363dfa8c24da@apps_vw-dilab_com"; // this.xclientId = "77869e21-e30a-4a92-b016-48ab7d3db1d8"; // this.scope = // "address profile badge birthdate birthplace nationalIdentifier nationality profession email vin phone nickname name picture mbb gallery openid"; // this.redirect = "myaudi:///"; // this.xrequest = "de.myaudi.mobile.assistant"; // this.responseType = "token%20id_token"; // // this.responseType = "code"; // this.xappversion = "3.22.0"; // this.xappname = "myAudi"; } if (this.config.type === "audietron") { this.type = "Audi"; this.country = "DE"; this.clientId = "f4d0934f-32bf-4ce4-b3c4-699a7049ad26@apps_vw-dilab_com"; this.scope = "address badge birthdate birthplace email gallery mbb name nationalIdentifier nationality nickname phone picture profession profile vin openid"; this.redirect = "myaudi:///"; this.responseType = "code"; this.xappversion = "4.14.1"; this.xappname = "myAudi"; this.xclientId = "59edf286-a9ca-4d34-9421-68da00f72dc8"; } if (this.config.type === "audidata") { this.type = "Audi"; this.country = "DE"; this.clientId = "ec6198b1-b31e-41ec-9a69-95d42d6497ed@apps_vw-dilab_com"; this.scope = "openid profile address email phone"; this.redirect = "acpp://de.audi.connectplugandplay/oauth2redirect/identitykit"; this.responseType = "code"; } if (this.config.type === "go") { this.type = ""; this.country = ""; this.clientId = "ac42b0fa-3b11-48a0-a941-43a399e7ef84@apps_vw-dilab_com"; this.xclientId = ""; this.scope = "openid%20profile%20address%20email%20phone"; this.redirect = "vwconnect%3A%2F%2Fde.volkswagen.vwconnect%2Foauth2redirect%2Fidentitykit"; this.xrequest = ""; this.responseType = "code"; this.xappversion = ""; this.xappname = ""; } if (this.config.type === "seatelli") { this.type = ""; this.country = ""; this.clientId = "d940d794-5945-48a3-84b1-44222c387800@apps_vw-dilab_com"; this.xclientId = ""; this.scope = "openid profile"; this.redirect = "Seat-elli-hub://opid"; this.xrequest = ""; this.responseType = "code"; this.xappversion = ""; this.xappname = ""; } if (this.config.type === "skodapower") { this.type = ""; this.country = ""; this.clientId = "b84ba8a1-7925-43c9-9963-022587faaac5@apps_vw-dilab_com"; this.xclientId = ""; this.scope = "openid profile"; this.redirect = "skoda-hub://opid"; this.xrequest = ""; this.responseType = "code"; this.xappversion = ""; this.xappname = ""; } if (!this.config.interval || this.config.interval < 0.5) { this.log.info("Interval of 0 is not allowed reset to 1"); this.config.interval = 1; } // if (this.config.type === "skodae") { // // this.log.info("Parking Postion is temporary disabled for Skoda E"); // if (this.config.interval < 10) { // this.log.info("Interval under 10min is temporary not allowed for Skoda E reset to 10min"); // this.config.interval = 10; // } // } this.tripTypes = []; if (this.config.tripShortTerm == true) { this.tripTypes.push("shortTerm"); } if (this.config.tripLongTerm == true) { this.tripTypes.push("longTerm"); } if (this.config.tripCyclic == true) { this.tripTypes.push("cyclic"); } this.login() .then(() => { this.log.info("Login successful"); this.setState("info.connection", true, true); this.extendObject("refresh", { type: "state", common: { name: "Refresh All States", type: "boolean", role: "button", write: true, }, native: {}, }); this.getPersonalData().then(() => { this.getVehicles() .then(() => { if (this.config.type !== "go") { this.vinArray.forEach((vin) => { if (this.config.type === "id" || this.config.type === "audietron") { this.getHomeRegion(vin); this.getIdStatus(vin).catch(() => { this.log.error("get id status Failed"); }); } else if (this.config.type === "seatcupra") { this.getSeatCupraStatus(vin); } else if (this.config.type === "audidata") { this.getAudiDataStatus(vin).catch(() => { this.log.error("get audi data status Failed"); }); } else if (this.config.type === "skodae") { this.getSkodaEStatus(vin); } else { this.getHomeRegion(vin) .catch(() => { this.log.debug("get home region Failed " + vin); }) .finally(() => { this.getVehicleData(vin).catch(() => { this.log.error("get vehicle data Failed"); }); this.getVehicleRights(vin).catch(() => { this.log.error("get vehicle rights Failed"); }); this.requestStatusUpdate(vin) .finally(() => { this.statesArray.forEach((state) => { if (state.path == "tripdata") { this.tripTypes.forEach((tripType) => { this.getVehicleStatus( vin, state.url, state.path, state.element, state.element2, state.element3, state.element4, tripType, ).catch(() => { this.log.debug("error while getting " + state.url); }); }); } else { this.getVehicleStatus( vin, state.url, state.path, state.element, state.element2, state.element3, state.element4, ).catch(() => { this.log.debug("error while getting " + state.url); }); } }); }) .catch(() => { this.log.error("status update Failed " + vin); }); }) .catch(() => { this.log.error("Error getting home region"); }); } }); } if (this.config.type !== "skodae" && this.config.type !== "seatcupra") { this.updateStatus(); } this.updateInterval && clearInterval(this.updateInterval); this.updateInterval = setInterval(() => { this.updateStatus(); }, this.config.interval * 60 * 1000); if (this.config.type !== "id" && this.config.type !== "skodae" && this.config.type !== "audietron") { if (this.config.forceinterval > 0) { this.fupdateInterval = setInterval(() => { if (this.config.type === "go") { this.getVehicles(); return; } this.vinArray.forEach((vin) => { this.requestStatusUpdate(vin).catch(() => { this.log.error("force status update Failed"); }); }); }, this.config.forceinterval * 60 * 1000); } } if (this.config.type === "seatelli" || this.config.type === "skodapower") { this.getElliData(this.config.type).catch(() => { this.log.error("get elli Failed"); }); } }) .catch(() => { this.log.error("Get Vehicles Failed"); }); }); }) .catch(() => { this.log.error("Login Failed"); this.log.error("Restart Adapter in 30min"); setTimeout(() => { this.log.error("Restart adapter"); this.restart(); }, 30 * 60 * 1000); }); this.subscribeStates("*"); } login() { // eslint-disable-next-line no-async-promise-executor return new Promise(async (resolve, reject) => { const nonce = this.getNonce(); const state = uuidv4(); this.log.info(`Login in with ${this.config.type}`); let [code_verifier, codeChallenge] = this.getCodeChallenge(); if (this.config.type === "seatelli" || this.config.type === "skodapower") { [code_verifier, codeChallenge] = this.getCodeChallengev2(); } const method = "GET"; const form = {}; let url = "https://identity.vwgroup.io/oidc/v1/authorize?client_id=" + this.clientId + "&scope=" + this.scope + "&response_type=" + this.responseType + "&redirect_uri=" + this.redirect + "&nonce=" + nonce + "&state=" + state; if ( this.config.type === "vw" || this.config.type === "vwv2" || this.config.type === "go" || this.config.type === "seatelli" || this.config.type === "skodae" || this.config.type === "skodapower" || this.config.type === "audidata" || this.config.type === "audietron" || this.config.type === "seatcupra" || this.config.type === "seatcupra2" ) { url += "&code_challenge=" + codeChallenge + "&code_challenge_method=S256"; } if (this.config.type === "audi") { url += "&ui_locales=de-DE%20de&prompt=login"; } if (this.config.type === "id" && this.type !== "Wc") { url = await this.receiveLoginUrl().catch(() => { this.log.warn("Failed to get login url"); }); if (!url) { url = "https://emea.bff.cariad.digital/user-login/v1/authorize?nonce=" + this.randomString(16) + "&redirect_uri=weconnect://authenticated"; } } const loginRequest = request( { method: method, url: url, headers: { "User-Agent": this.userAgent, Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3", "Accept-Language": "en-US,en;q=0.9", "Accept-Encoding": "gzip, deflate", "x-requested-with": this.xrequest, "upgrade-insecure-requests": 1, }, jar: this.jar, form: form, gzip: true, followAllRedirects: true, }, (err, resp, body) => { if (err || (resp && resp.statusCode >= 400)) { if (this.type === "Wc") { if (err && err.message && err.message === "Invalid protocol: wecharge:") { this.log.debug("Found WeCharge connection"); this.getTokens(loginRequest, code_verifier, reject, resolve); } else { this.log.debug("No WeCharge found, cancel login"); resolve(); } return; } if (err && err.message && err.message.indexOf("Invalid protocol:") !== -1) { this.log.debug("Found Token"); this.getTokens(loginRequest, code_verifier, reject, resolve); return; } this.log.error("Failed in first login step "); err && this.log.error(err); resp && this.log.error(resp.statusCode.toString()); body && this.log.error(JSON.stringify(body)); err && err.message && this.log.error(err.message); loginRequest && loginRequest.uri && loginRequest.uri.query && this.log.debug(loginRequest.uri.query.toString()); reject(); return; } try { let form = {}; if (body.indexOf("emailPasswordForm") !== -1) { this.log.debug("parseEmailForm"); form = this.extractHidden(body); form["email"] = this.config.user; } else { if (this.type === "Wc") { resolve(); return; } this.log.error("No Login Form found for type: " + this.type); this.log.debug(JSON.stringify(body)); reject(); return; } request.post( { url: "https://identity.vwgroup.io/signin-service/v1/" + this.clientId + "/login/identifier", headers: { "Content-Type": "application/x-www-form-urlencoded", "User-Agent": this.userAgent, Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3", "Accept-Language": "en-US,en;q=0.9", "Accept-Encoding": "gzip, deflate", "x-requested-with": this.xrequest, }, form: form, jar: this.jar, gzip: true, followAllRedirects: true, }, (err, resp, body) => { if (err || (resp && resp.statusCode >= 400)) { this.log.error("Failed to get login identifier"); err && this.log.error(err); resp && this.log.error(resp.statusCode.toString()); body && this.log.error(JSON.stringify(body)); reject(); return; } try { if (body.indexOf("emailPasswordForm") !== -1) { this.log.debug("emailPasswordForm2"); /* const stringJson =body.split("window._IDK = ")[1].split(";")[0].replace(/\n/g, "") const json =stringJson.replace(/(['"])?([a-z0-9A-Z_]+)(['"])?:/g, '"$2": ').replace(/'/g, '"') const jsonObj = JSON.parse(json); */ form = { _csrf: body.split("csrf_token: '")[1].split("'")[0], email: this.config.user, password: this.config.password, hmac: body.split('"hmac":"')[1].split('"')[0], relayState: body.split('"relayState":"')[1].split('"')[0], }; } else { this.log.error("No Login Form found. Please check your E-Mail in the app."); this.log.debug(JSON.stringify(body)); reject(); return; } request.post( { url: "https://identity.vwgroup.io/signin-service/v1/" + this.clientId + "/login/authenticate", headers: { "Content-Type": "application/x-www-form-urlencoded", "User-Agent": this.userAgent, Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3", "Accept-Language": "en-US,en;q=0.9", "Accept-Encoding": "gzip, deflate", "x-requested-with": this.xrequest, }, form: form, jar: this.jar, gzip: true, followAllRedirects: false, }, (err, resp, body) => { if (err || (resp && resp.statusCode >= 400)) { this.log.error("Failed to get login authenticate"); err && this.log.error(err); resp && this.log.error(resp.statusCode.toString()); body && this.log.error(JSON.stringify(body)); reject(); return; } try { this.log.debug(JSON.stringify(body)); this.log.debug(JSON.stringify(resp.headers)); if ( resp.headers.location.split("&").length <= 2 || resp.headers.location.indexOf("/terms-and-conditions?") !== -1 ) { this.log.warn(resp.headers.location); this.log.warn( "No valid userid, please check username and password or visit this link or logout and login in your app account:", ); this.log.warn("Bitte in die App einloggen und die Nutzungsbedingungen akzeptieren."); this.log.warn("https://" + resp.request.host + resp.headers.location); this.log.warn("For Skoda: https://skodaid.vwgroup.io/landing-page"); this.log.warn("For VW: https://vwid.vwgroup.io/landing-page"); this.log.warn("Try to auto accept new consent"); request.get( { url: "https://" + resp.request.host + resp.headers.location, jar: this.jar, headers: { "User-Agent": this.userAgent, Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3", "Accept-Language": "en-US,en;q=0.9", "Accept-Encoding": "gzip, deflate", "x-requested-with": this.xrequest, }, followAllRedirects: true, gzip: true, }, (err, resp, body) => { this.log.debug(body); const form = this.extractHidden(body); //check for empty form object if (Object.keys(form).length === 0 && form.constructor === Object) { try { const stringJson = body.split("window._IDK = ")[1].split("</")[0]; let json = stringJson.replace(/([{,]\s*)(\w+)\s*:/g, '$1"$2":'); // Add quotes around property names json = json.replace(/'/g, '"'); json = json.replace(/,\s*}/g, "}"); // Remove trailing commas const parsedJson = JSON.parse(json); form._csrf = parsedJson.csrf_token; form.hmac = parsedJson.templateModel.hmac; form.relayState = parsedJson.templateModel.relayState; form.legalDocuments = parsedJson.templateModel.legalDocuments; form.countryOfResidence = "DE"; form.countryOfJurisdiction = "DE"; } catch (error) { this.log.error( "Error in consent form. Please accept the Data Privacy Statement in the app after relogin", ); this.log.error(error); reject(); return; } } const url = "https://" + resp.request.host + resp.req.path.split("?")[0]; this.log.debug(JSON.stringify(form)); request.post( { url: url, jar: this.jar, headers: { "Content-Type": "application/x-www-form-urlencoded", "User-Agent": this.userAgent, Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3", "Accept-Language": "en-US,en;q=0.9", "Accept-Encoding": "gzip, deflate", "x-requested-with": this.xrequest, }, form: qs .stringify(form) .replace(/true/g, "yes") .replace(/false/g, "no") .replace(/%5D%5B/g, "%5D.") .replace(/%5D=/g, "="), followAllRedirects: true, gzip: true, }, (err, resp, body) => { if (err && err.message.indexOf("Invalid protocol:") === 0) { this.log.info("Auto accept succesful. Restart adapter in 10sec"); setTimeout(() => { this.restart(); }, 10 * 1000); return; } if ( (err && err.message.indexOf("Invalid protocol:") !== -1) || (resp && resp.statusCode >= 400) ) { this.log.warn("Failed to auto accept"); err && this.log.error(err); resp && this.log.error(resp.statusCode.toString()); body && this.log.error(JSON.stringify(body)); reject(); return; } this.log.info("Auto accept succesful. Restart adapter in 10sec"); setTimeout(() => { this.restart(); }, 10 * 1000); }, ); }, ); reject(); return; } this.config.userid = resp.headers.location.split("&")[2].split("=")[1]; if (!this.stringIsAValidUrl(resp.headers.location)) { if (resp.headers.location.indexOf("&error=") !== -1) { const location = resp.headers.location; this.log.error( "Error: " + location.substring(location.indexOf("error="), location.length - 1), ); } else { this.log.error("No valid login url, please download the log and visit:"); this.log.error("http://" + resp.request.host + resp.headers.location); } reject(); return; } let getRequest = request.get( { url: resp.headers.location || "", headers: { "User-Agent": this.userAgent, Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3", "Accept-Language": "en-US,en;q=0.9", "Accept-Encoding": "gzip, deflate", "x-requested-with": this.xrequest, }, jar: this.jar, gzip: true, followAllRedirects: true, }, (err, resp, body) => { if (err) { this.log.debug(err); this.getTokens(getRequest, code_verifier, reject, resolve); } else { this.log.debug(body); this.log.warn( "No Token received visiting url and accept the permissions or login in the App and accept manually", ); this.log.info(getRequest.uri.href); const form = this.extractHidden(body); getRequest = request.post( { url: getRequest.uri.href, headers: { "Content-Type": "application/x-www-form-urlencoded", "User-Agent": this.userAgent, Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3", "Accept-Language": "en-US,en;q=0.9", "Accept-Encoding": "gzip, deflate", "x-requested-with": this.xrequest, referer: getRequest.uri.href, }, form: form, jar: this.jar, gzip: true, followAllRedirects: true, }, (err, resp, body) => { if (err) { this.getTokens(getRequest, code_verifier, reject, resolve); } else { this.log.error( "No Token received. Please try to logout and login in the latest MySkoda or MySeat App", ); try { this.log.debug(JSON.stringify(body)); } catch (err) { this.log.error(err); reject(); } } }, ); } }, ); } catch (err2) { this.log.error( "Login was not successful, please check your login credentials and selected type", ); err && this.log.error(err); this.log.error(err2); this.log.error(err2.stack); reject(); } }, ); } catch (err) { this.log.error(err); reject(); } }, ); } catch (err) { this.log.error(err); reject(); } }, ); }); } async cleanObjects(vin) { let remoteState = await this.getObjectAsync(vin + ".general.systemId"); if (remoteState) { this.log.info("clean old states" + vin); await this.delObjectAsync(vin, { recursive: true }); } remoteState = await this.getObjectAsync(vin + ".general.capabilities"); if (remoteState) { this.log.info("clean old states" + vin); await this.delObjectAsync(vin, { recursive: true }); } if (this.config.type === "seatcupra") { remoteState = await this.getObjectAsync(vin + ".remote.targetTemperatureInCelsius"); if (!remoteState) { this.log.info("clean old states" + vin); await this.delObjectAsync(vin, { recursive: true }); } } } updateStatus() { if (this.config.type === "go") { this.getVehicles(); return; } else if (this.config.type === "skodae") { this.vinArray.forEach((vin) => { this.getSkodaEStatus(vin); }); if (this.pairedWallbox) { this.getWcData(this.config.historyLimit); } } else if (this.config.type === "audidata") { this.vinArray.forEach((vin) => { this.getAudiDataStatus(vin).catch(() => { this.log.error("get audi data status Failed"); }); }); } else if (this.config.type === "id") { this.vinArray.forEach((vin) => { this.getIdStatus(vin).catch(() => { this.log.error("get id status Failed"); this.refreshIDToken().catch(() => {}); }); if (this.config.type === "id" && this.pairedWallbox) { this.getWcData(this.config.historyLimit); } }); return; } else if (this.config.type === "audietron") { this.vinArray.forEach((vin) => { this.getIdStatus(vin).catch(() => { this.log.error("get id status Failed"); this.refreshTokenv2().catch(() => {}); }); }); return; } else if (this.config.type === "seatcupra") { this.vinArray.forEach((vin) => { this.getSeatCupraStatus(vin); }); return; } else if (this.config.type === "seatelli" || this.config.type === "skodapower") { this.getElliData(this.config.type).catch(() => { this.log.error("get elli Failed"); }); return; } else { this.vinArray.forEach((vin) => { this.statesArray.forEach((state) => { if (state.path == "tripdata") { this.tripTypes.forEach((tripType) => { this.getVehicleStatus( vin, state.url, state.path, state.element, state.element2, null, null, tripType, ).catch(() => { this.log.debug("error while getting " + state.url); }); }); } else { this.getVehicleStatus(vin, state.url, state.path, state.element, state.element2).catch(() => { this.log.debug("error while getting " + state.url); }); } }); }); } } receiveLoginUrl() { return new Promise((resolve, reject) => { request( { method: "GET", url: "https://emea.bff.cariad.digital/user-login/v1/authorize?nonce=" + this.randomString(16) + "&redirect_uri=weconnect://authenticated", headers: { "user-agent": this.userAgent, accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", "accept-language": "de-de", }, jar: this.jar, gzip: true, followAllRedirects: false, }, (err, resp, body) => { if (err || (resp && resp.statusCode >= 400)) { this.log.error("Failed in receive login url "); err && this.log.error(err); resp && this.log.error(resp.statusCode.toString()); body && this.log.error(JSON.stringify(body)); reject(); return; } resolve(resp.request.href); }, ); }); } replaceVarInUrl(url, vin, tripType) { const curHomeRegion = this.homeRegion[vin] || "https://msg.volkswagen.de"; return url .replace("/$vin", "/" + vin + "") .replace("$homeregion/", curHomeRegion + "/") .replace("/$type/", "/" + this.type + "/") .replace("/$country/", "/" + this.country + "/") .replace("/$tripType", "/" + tripType); } getQmauth() { const timestamp = parseInt(Date.now() / 100000); this.log.debug(timestamp.toString()); //credits to https://github.com/arjenvrh/audi_connect_ha/blob/master/custom_components/audiconnect/audi_services.py const xqmauth_secret = Buffer.from([ 26, 256 - 74, 256 - 103, 37, 256 - 84, 23, 256 - 102, 256 - 86, 78, 256 - 125, 256 - 85, 256 - 26, 113, 256 - 87, 71, 109, 23, 100, 24, 256 - 72, 91, 256 - 41, 6, 256 - 15, 67, 108, 256 - 95, 91, 256 - 26, 71, 256 - 104, 256 - 100, ]); const xqmauth_val = crypto.createHmac("sha256", xqmauth_secret).update(timestamp.toString()).digest("hex"); this.log.debug(timestamp.toString()); return "v1:01da27b0:" + xqmauth_val; } getTokensv2(getRequest, code_verifier, reject, resolve) { const url = getRequest.uri.query; this.log.debug(url); const queries = qs.parse(url); const body = { client_id: this.clientId, grant_type: "authorization_code", code: queries.code, redirect_uri: "myaudi:///", response_type: "token id_token", code_verifier: code_verifier, }; const qmAuth = this.getQmauth(); this.log.debug(qmAuth); this.log.debug(JSON.stringify(body)); request( { method: "POST", url: "https://emea.bff.cariad.digital/login/v1/idk/token", headers: { accept: "application/json", "content-type": "application/x-www-form-urlencoded; charset=utf-8", "accept-charset": "utf-8", "x-qmauth": qmAuth, "accept-language": "de-de", "user-agent": this.userAgent, }, jar: this.jar, gzip: true, followAllRedirects: true, body: qs.stringify(body), }, (err, resp) => { if (err || (resp && resp.statusCode >= 400)) { this.log.error("Failed get tokensv2. Please check your if your local time is correct"); err && this.log.error(err); resp && this.log.error(resp.statusCode.toString()); resp && resp.body && this.log.error(JSON.stringify(resp.body)); reject(); return; } const idktokens = JSON.parse(resp.body); this.config.atoken = idktokens.access_token; this.config.rtoken = idktokens.refresh_token; request( { method: "POST", url: "https://emea.bff.cariad.digital/login/v1/audi/token", headers: { accept: "application/json", "content-type": "application/json; charset=utf-8", "accept-charset": "utf-8", "x-app-version": "4.13.0", "x-app-name": "myAudi", "accept-language": "de-de", "user-agent": this.userAgent, }, jar: this.jar, gzip: true, followAllRedirects: false, body: JSON.stringify({ token: this.config.atoken, grant_type: "id_token", stage: "live", config: "myaudi", }), }, (err, resp) => { if (err || (resp && resp.statusCode >= 400)) { this.log.error("failed get audi token"); err && this.log.error(err); resp && this.log.error(resp.statusCode.toString()); body && this.log.error(JSON.stringify(body)); reject(); return; } if (this.config.type === "audi") { // eslint-disable-next-line this.getVWToken({}, jwtid_token, reject, resolve); return; } this.aaztoken = JSON.parse(resp.body); this.refreshTokenInterval && clearInterval(this.refreshTokenInterval); this.refreshTokenInterval = setInterval(() => { this.refreshTokenv2().catch(() => {}); }, 0.9 * 60 * 60 * 1000); // 0.9hours this.getVWToken({}, idktokens.id_token, reject, resolve); }, ); }, ); } getTokens(getRequest, code_verifier, reject, resolve) { if (this.config.type === "audietron") { this.getTokensv2(getRequest, code_verifier, reject, resolve); return; } let hash = ""; if (getRequest.uri.hash) { hash = getRequest.uri.hash; } else { hash = getRequest.uri.query; } const hashArray = hash.split("&"); // eslint-disable-next-line no-unused-vars let state; let jwtauth_code; let jwtaccess_token; let jwtid_token; let jwtstate; hashArray.forEach((hash) => { const harray = hash.split("="); if (harray[0] === "#state" || harray[0] === "state") { state = harray[1]; } if (harray[0] === "code") { jwtauth_code = harray[1]; } if (harray[0] === "access_token") { jwtaccess_token = harray[1]; } if (harray[0] === "id_token") { jwtid_token = harray[1]; } if (harray[0] === "#state") { jwtstate = harray[1]; } }); // const state = hashArray[0].substring(hashArray[0].indexOf("=") + 1); // const jwtauth_code = hashArray[1].substring(hashArray[1].indexOf("=") + 1); // const jwtaccess_token = hashArray[2].substring(hashArray[2].indexOf("=") + 1); // const jwtid_token = hashArray[5].substring(hashArray[5].indexOf("=") + 1); let method = "POST"; let body = "auth_code=" + jwtauth_code + "&id_token=" + jwtid_token; let url = "https://tokenrefreshservice.apps.emea.vwapps.io/exchangeAuthCode"; let headers = { // "user-agent": this.userAgent, "X-App-version": this.xappversion, "content-type": "application/x-www-form-urlencoded", "x-app-name": this.xappname, accept: "application/json", }; if (this.config.type === "vw" || this.config.type === "vwv2") { body += "&code_verifier=" + code_verifier; } else { let brand = this.config.type === "skodae" ? "skoda" : this.config.type; if (this.config.type === "seatcupra2") { brand = "cupra"; } body += "&brand=" + brand; } if (this.config.type === "seatcupra2") { body += "&code_verifier=" + code_verifier; } if (this.config.type === "skodae") { const parsedParameters = qs.parse(hash); // this.config.atoken = parsedParameters.access_token; let systemId = "TECHNICAL"; if (this.clientId === "7f045eee-7003-4379-9968-9355ed2adb06@apps_vw-dilab_com") { systemId = "CONNECT"; } method = "POST"; url = "https://mysmob.api.connect.skoda-auto.cz/api/v1/authentication/exchange-authorization-code?tokenType=" + systemId; body = JSON.stringify({ code: parsedParameters.code, redirectUri: "myskoda://redirect/login/", verifier: code_verifier, }); headers = { accept: "*/*", // authorization: "Bearer " + parsedParameters.id_token, "content-type": "application/json", "user-agent": this.skodaUserAgent, "accept-language": "de-de", }; } if (this.config.type === "go") { url = "https://dmp.apps.emea.vwapps.io/mobility-platform/token"; body = "code=" + jwtauth_code + "&client_id=" + this.clientId + "&redirect_uri=vwconnect://de.volkswagen.vwconnect/oa