-
-
Save mountbatt/772e4512089802a2aa2622058dd1ded7 to your computer and use it in GitHub Desktop.
| // Variables used by Scriptable. | |
| // These must be at the very top of the file. Do not edit. | |
| // icon-color: light-gray; icon-glyph: car; | |
| // version 2025-06-29 | |
| // latest changes: | |
| // not showing invalid charging kW when not available - thanks to dehsgr! <3 | |
| // add your my-renault account data: | |
| let myRenaultUser = "your_email" // email | |
| let myRenaultPass = "your_password" // password | |
| // set your ZOE Model (Phase 1 or 2) // bitte eingeben! | |
| let ZOE_Phase = "2" // "1" or "2" | |
| // set your battery size in kWh // bitte eingeben! | |
| let ZOE_Battery = "52" // "52" or "41" or "22" or "21" | |
| // should we use apple-maps or google maps? | |
| let mapProvider = "apple" // "apple" or "google" | |
| // optional: | |
| // change that number to eg. 2 if you want to select an another car from your account. | |
| // 1 is the first car, 2 will be the second. | |
| let carNumber = 1; | |
| // optional | |
| // account number | |
| // if you have problems with accessing the data, try to change this number from 0 to 1 | |
| let accountNumber = 0; | |
| // enter your VIN / FIN if you have any problems | |
| // leave it empty if everything works // leer lassen, wenn alles läuft | |
| let VIN = "" // starts with VF1... enter like this: "VF1XXXXXXXXX" | |
| // Language Strings | |
| // GERMAN / DEUTSCH | |
| let text_ladestand = "Ladestand" | |
| let text_reichweite = "Reichweite" | |
| let text_restenergie = "Restenergie" | |
| let text_kilometerstand = "Kilometerstand" | |
| let text_position = "Position" | |
| let text_klimatisierung = "Klimatisierung" | |
| let text_karte_oeffnen = "➤ Karte öffnen" | |
| let text_starten = "➤ Starten" | |
| let text_stoppen = "➤ Stoppen" | |
| let text_wird_geladen = "⚡ Wird geladen …" | |
| let text_gekoppelt = "🟢 Gekoppelt" | |
| let text_entkoppelt = "⚫ Entkoppelt" | |
| let text_laden_starten = "➤ Laden starten" | |
| let text_akkutemp = "Akkutemp." | |
| /* | |
| // ENGLISH | |
| let text_ladestand = "Charge level" | |
| let text_reichweite = "Range" | |
| let text_restenergie = "Remaining energy" | |
| let text_kilometerstand = "Mileage" | |
| let text_position = "Position" | |
| let text_klimatisierung = "Climate control" | |
| let text_karte_oeffnen = "➤ Open map" | |
| let text_starten = "➤ Start" | |
| let text_stoppen = "➤ Stop" | |
| let text_wird_geladen = "⚡ Charging..." | |
| let text_gekoppelt = "🟢 Connected" | |
| let text_entkoppelt = "⚫ Disconnected" | |
| let text_laden_starten = "➤ Start charging" | |
| let text_akkutemp = "Battery temp." | |
| */ | |
| /* | |
| // FRENCH: | |
| let text_ladestand = "Niveau de charge" | |
| let text_reichweite = "Autonomie" | |
| let text_restenergie = "Énergie restante" | |
| let text_kilometerstand = "Kilométrage" | |
| let text_position = "Position" | |
| let text_klimatisierung = "Climatisation" | |
| let text_karte_oeffnen = "➤ Ouvrir la carte" | |
| let text_starten = "➤ Démarrer" | |
| let text_stoppen = "➤ Arrêter" | |
| let text_wird_geladen = "⚡ En charge ..." | |
| let text_gekoppelt = "🟢 Connecté" | |
| let text_entkoppelt = "⚫ Déconnecté" | |
| let text_laden_starten = "➤ Démarrer la charge" | |
| let text_akkutemp = "Temp. de la batterie" | |
| */ | |
| // do not edit | |
| let kamareonURL = "https://api-wired-prod-1-euw1.wrd-aws.com" | |
| let kamareonAPI = "YjkKtHmGfaceeuExUDKGxrLZGGvtVS0J" | |
| let gigyaURL = "https://accounts.eu1.gigya.com" | |
| let gigyaAPI = "3_7PLksOyBRkHv126x5WhHb-5pqC1qFR8pQjxSeLB6nhAnPERTUlwnYoznHSxwX668" // austria: "3__B4KghyeUb0GlpU62ZXKrjSfb7CPzwBS368wioftJUL5qXE0Z_sSy0rX69klXuHy" | |
| const timenow = new Date().toJSON().slice(0, 13).replace(/-/g, '').replace(/T/g, '-') //20201028-14 (14 = hour) | |
| // clear everything from keychain if we are on an other day | |
| if (Keychain.contains('lastJWTCall') && Keychain.get('lastJWTCall') != timenow) { | |
| clearKeychain() | |
| //console.log("Keychain cleared") | |
| } | |
| // clear keychain, if script gets called with action parameters (to get new tokens) | |
| if (args.queryParameters.action != "") { | |
| clearKeychain() | |
| //console.log("Keychain cleared cause of action parameters") | |
| } | |
| function clearKeychain() { | |
| if (Keychain.contains('VIN')) { Keychain.remove('VIN') } | |
| if (Keychain.contains('carPicture')) { Keychain.remove('carPicture') } // enable if picture is wrong | |
| if (Keychain.contains('account_id')) { Keychain.remove('account_id') } | |
| if (Keychain.contains('gigyaJWTToken')) { Keychain.remove('gigyaJWTToken') } | |
| if (Keychain.contains('gigyaCookieValue')) { Keychain.remove('gigyaCookieValue') } | |
| if (Keychain.contains('gigyaPersonID')) { Keychain.remove('gigyaPersonID') } | |
| } | |
| if (VIN && VIN != "") { | |
| Keychain.set('VIN', VIN) | |
| } | |
| const widget = new ListWidget() | |
| await createWidget() | |
| // used for debugging if script runs inside the app | |
| if (!config.runsInWidget) { | |
| await widget.presentMedium() | |
| } | |
| Script.setWidget(widget) | |
| Script.complete() | |
| // build the widget | |
| async function createWidget(items) { | |
| // get all data in a single variable | |
| const data = await getData() | |
| console.log(data) | |
| //widget.refreshAfterDate = new Date(Date.now() + 300) // dont know if this works | |
| widget.setPadding(10, 0, 10, 20) | |
| const wrap = widget.addStack() | |
| wrap.layoutHorizontally() | |
| wrap.topAlignContent() | |
| wrap.spacing = 15 | |
| const column0 = wrap.addStack() | |
| column0.layoutVertically() | |
| if (data.carPicture) { | |
| const icon = await getImage("my-renault-car-" + VIN + ".png", data.carPicture) | |
| //console.log("getting my-renault-car-"+VIN+".png") | |
| //console.log("current icon: " + data.carPicture) | |
| let CarStack = column0.addStack() | |
| let iconImg = CarStack.addImage(icon) | |
| // simple hack if we have a phase 1 model (no location data & no hvac-status available) – resize car-image | |
| // not the smartest solution - but i try to check if the results show only 1 column. | |
| // if column2 is empty, we have to resizes the car-image for better styling | |
| if (typeof(data.locationStatus) == 'undefined' && typeof(data.hvacStatus) == 'undefined') { | |
| iconImg.imageSize = new Size(130, 73) | |
| } | |
| } | |
| column0.addSpacer(8) | |
| if (typeof(data.batteryStatus) != 'undefined') { | |
| let plugIcon | |
| let plugStateLabel | |
| let plugStateUrl | |
| let scriptName = encodeURIComponent(Script.name()) | |
| const PlugWrap = column0.addStack() | |
| PlugWrap.layoutHorizontally() | |
| //PlugWrap.setPadding(0,15,0,15) | |
| if (data.batteryStatus.attributes.plugStatus != 1) { | |
| //plugIcon = await getImage("zoe-plug-off.png", "") | |
| plugStateLabel = text_entkoppelt | |
| } else { | |
| //plugIcon = await getImage("zoe-plug-on.png", "") | |
| plugStateLabel = text_gekoppelt | |
| } | |
| if (data.batteryStatus.attributes.chargingStatus == "1.0") { | |
| plugStateLabel = text_wird_geladen | |
| } | |
| if (data.batteryStatus.attributes.plugStatus == 1 && data.batteryStatus.attributes.chargingStatus == "0") { | |
| plugStateLabel = text_laden_starten | |
| plugStateUrl = `scriptable:///run?scriptName=${scriptName}&action=start_charge`; | |
| } | |
| const PlugText = PlugWrap.addStack() | |
| PlugText.setPadding(0, 10, 0, 0) | |
| PlugText.layoutVertically() | |
| plugStateLabel = PlugText.addText(plugStateLabel) | |
| plugStateLabel.font = Font.regularSystemFont(10) | |
| plugStateLabel.url = plugStateUrl | |
| PlugText.addSpacer(6) | |
| if (data.batteryStatus.attributes.chargingStatus == "1.0") { // must be 1.0, debug = 0 | |
| // (Akku minus verf. Energie) geteilt durch Restzeit = Ladegeschwindigkeit (by Marc) | |
| let batteryAvailableEnergy = data.batteryStatus.attributes.batteryAvailableEnergy; | |
| let chargingInstantaneousPower = data.batteryStatus.attributes.chargingInstantaneousPower | |
| chargingInstantaneousPower = Math.round(chargingInstantaneousPower) | |
| // check if the numbers are in Watt or kW | |
| if (chargingInstantaneousPower > 150) { | |
| // if over 200, we believe the value is in watt :-) | |
| chargingInstantaneousPower = chargingInstantaneousPower / 1000 | |
| } | |
| //console.log('chargingInstantaneousPower: ' + chargingInstantaneousPower) | |
| chargingInstantaneousPower = Math.round(chargingInstantaneousPower).toLocaleString() | |
| //console.log('chargingInstantaneousPower rounded: ' + chargingInstantaneousPower) | |
| let chargingPower = chargingInstantaneousPower | |
| if (ZOE_Battery) { | |
| chargingPower = (ZOE_Battery - batteryAvailableEnergy) / (data.batteryStatus.attributes.chargingRemainingTime / 60) | |
| //console.log("chargingPower calculated: " + chargingPower); | |
| chargingPower = chargingPower.toFixed(1).toLocaleString() | |
| } | |
| //console.log('chargingPower final result: ' + chargingPower) | |
| let chargingRemainingTime = time_convert(data.batteryStatus.attributes.chargingRemainingTime) | |
| chargingRemainingTimeString = chargingRemainingTime + " h" | |
| chargeStateLabel = " " + (chargingPower > 0 ? chargingPower + " kW" + " | " : "") + chargingRemainingTimeString | |
| chargeStateLabel = PlugText.addText(chargeStateLabel) | |
| chargeStateLabel.font = Font.regularSystemFont(10) | |
| PlugText.addSpacer(2) | |
| } | |
| } | |
| const column1 = wrap.addStack() | |
| column1.layoutVertically() | |
| //column1.addSpacer(3) | |
| // simple quota-limit check: | |
| // (battery status is the first request – if it reports nothing, we can be sure, that there will be no other data available at the moment) | |
| if (!data.batteryStatus || typeof(data.batteryStatus) == "undefined") { | |
| if (config.runsInWidget) { // only in widget | |
| throw new Error('Quota Limit! – Datenabruf zur Zeit nicht möglich. Später nochmals versuchen oder bei Renault beschweren.') | |
| } else { | |
| console.log('Quota Limit! – Datenabruf zur Zeit nicht möglich. Später nochmals versuchen oder bei Renault beschweren.') | |
| } | |
| } | |
| if (typeof(data.batteryStatus) != 'undefined') { | |
| let BatteryStack = column1.addStack() | |
| BatteryStack.layoutVertically() | |
| const batteryStatusLabel = BatteryStack.addText(text_ladestand) | |
| batteryStatusLabel.font = Font.mediumSystemFont(12) | |
| const batteryStatusVal = BatteryStack.addText(data.batteryStatus.attributes.batteryLevel.toString() + " %") | |
| batteryStatusVal.font = Font.boldSystemFont(16) | |
| column1.addSpacer(10) | |
| // push Message if maxSoC reachead | |
| /* under development! */ | |
| /* | |
| let maxSoC = 62 | |
| // if(batteryStatusVal == maxSoC && data.batteryStatus.attributes.chargingStatus != "-1.0"){ | |
| const delaySeconds = 1; | |
| let currentDate = new Date; | |
| let newDate = new Date(currentDate.getTime() + (delaySeconds * 1000)); | |
| chargeFull = new Notification() | |
| chargeFull.identifier = "maxSoCReached" | |
| chargeFull.title = "🔋 Geladen" | |
| chargeFull.body = "Die Batterie Deines Fahrzeugs wurde zu " + maxSoC + " % geladen!" | |
| chargeFull.sound = "complete" | |
| chargeFull.setTriggerDate(newDate); | |
| chargeFull.schedule() | |
| // } */ | |
| } | |
| if (typeof(data.batteryStatus) != 'undefined') { | |
| let RangeStack = column1.addStack() | |
| RangeStack.layoutVertically() | |
| const RangeStatusLabel = RangeStack.addText(text_reichweite) | |
| RangeStatusLabel.font = Font.mediumSystemFont(12) | |
| const RangeStatusVal = RangeStack.addText(data.batteryStatus.attributes.batteryAutonomy.toString() + " km") | |
| RangeStatusVal.font = Font.boldSystemFont(16) | |
| column1.addSpacer(10) | |
| } | |
| if (ZOE_Phase == 1 && typeof(data.batteryStatus) != 'undefined') { | |
| if (typeof(data.batteryStatus.attributes.batteryTemperature) != 'undefined') { | |
| let TempStack = column1.addStack() | |
| TempStack.layoutVertically() | |
| const TempStatusLabel = TempStack.addText(text_akkutemp) | |
| TempStatusLabel.font = Font.mediumSystemFont(12) | |
| const TempStatusVal = TempStack.addText(data.batteryStatus.attributes.batteryTemperature.toString() + " °C") | |
| TempStatusVal.font = Font.boldSystemFont(16) | |
| } | |
| } | |
| if (ZOE_Phase == 2 && typeof(data.batteryStatus) != 'undefined') { | |
| if (typeof(data.batteryStatus.attributes.batteryAvailableEnergy) != 'undefined') { | |
| let AvEnergyStack = column1.addStack() | |
| AvEnergyStack.layoutVertically() | |
| const AvEnergyStatusLabel = AvEnergyStack.addText(text_restenergie) | |
| AvEnergyStatusLabel.font = Font.mediumSystemFont(12) | |
| const AvEnergyStatusVal = AvEnergyStack.addText(data.batteryStatus.attributes.batteryAvailableEnergy.toString() + " kWh") | |
| AvEnergyStatusVal.font = Font.boldSystemFont(16) | |
| } | |
| } | |
| const column2 = wrap.addStack() | |
| column2.layoutVertically() | |
| //column2.addSpacer(3) | |
| if (typeof(data.cockpitStatus) != 'undefined') { | |
| let MileageStack = column2.addStack() | |
| MileageStack.layoutVertically() | |
| const MileageStatusLabel = MileageStack.addText(text_kilometerstand) | |
| MileageStatusLabel.font = Font.mediumSystemFont(12) | |
| let mileage = Math.round(data.cockpitStatus.attributes.totalMileage).toLocaleString() | |
| const MileageStatusVal = MileageStack.addText(mileage.toString() + " km") | |
| MileageStatusVal.font = Font.boldSystemFont(16) | |
| column2.addSpacer(10) | |
| } | |
| if (typeof(data.locationStatus) != 'undefined') { | |
| let LocationStack = column2.addStack() | |
| LocationStack.spacing = 2 | |
| LocationStack.layoutVertically() | |
| const LocationLabel = LocationStack.addText(text_position) | |
| LocationLabel.font = Font.mediumSystemFont(12) | |
| const LocationVal = LocationStack.addText(text_karte_oeffnen) | |
| LocationVal.font = Font.boldSystemFont(12) | |
| if (mapProvider == "google") { | |
| // https://www.google.com/maps/search/?api=1&query=58.698017,-152.522067 | |
| LocationVal.url = "https://www.google.com/maps/search/?api=1&query=" + data.locationStatus.attributes.gpsLatitude + "," + data.locationStatus.attributes.gpsLongitude | |
| } else { | |
| // fallback to apple… | |
| // http://maps.apple.com/?ll=50.894967,4.341626 | |
| LocationVal.url = "http://maps.apple.com/?q=ZOE&ll=" + data.locationStatus.attributes.gpsLatitude + "," + data.locationStatus.attributes.gpsLongitude | |
| } | |
| //LocationStack.addSpacer(0.5) | |
| column2.addSpacer(12) | |
| } | |
| //if(typeof(data.hvacStatus) != 'undefined'){ // we have to uncomment this later! | |
| let AcStack = column2.addStack() | |
| AcStack.spacing = 2 | |
| AcStack.layoutVertically() | |
| const AcLabel = AcStack.addText(text_klimatisierung) | |
| AcLabel.font = Font.mediumSystemFont(12) | |
| // create a self-opening url to run the start_ac function | |
| // could be nicer, but seems to work at the moment. | |
| let scriptName = encodeURIComponent(Script.name()) | |
| let AcVal | |
| let ac_url | |
| if (args.queryParameters.action == 'start_ac') { | |
| AcVal = AcStack.addText(text_stoppen) | |
| ac_url = `scriptable:///run?scriptName=${scriptName}&action=stop_ac`; | |
| } else { | |
| AcVal = AcStack.addText(text_starten) | |
| ac_url = `scriptable:///run?scriptName=${scriptName}&action=start_ac`; | |
| } | |
| AcVal.font = Font.boldSystemFont(12) | |
| AcVal.url = ac_url | |
| //} // we have to uncomment this later! | |
| } | |
| // fetch all data | |
| async function getData() { | |
| // we are going now a long way through multiple servers to get access to our data | |
| // 1. fetch session and user data from gigya | |
| let gigyaCookieValue | |
| let gigyaPersonID | |
| if (Keychain.contains('gigyaCookieValue') && Keychain.get('gigyaCookieValue') != "") { | |
| gigyaCookieValue = Keychain.get('gigyaCookieValue') | |
| } | |
| //console.log('gigyaCookieValue (from keychain): ' + gigyaCookieValue) | |
| if (Keychain.contains('gigyaPersonID') && Keychain.get('gigyaPersonID') != "") { | |
| gigyaPersonID = Keychain.get('gigyaPersonID') | |
| } | |
| //console.log('gigyaPersonID (from keychain): ' + gigyaPersonID) | |
| if (gigyaCookieValue == "" || gigyaPersonID == "" || | |
| typeof(gigyaCookieValue) == "undefined" || typeof(gigyaPersonID) == "undefined") { | |
| let url = gigyaURL + '/accounts.login?loginID=' + encodeURIComponent(myRenaultUser) + '&password=' + encodeURIComponent(myRenaultPass) + '&include=data&apiKey=' + gigyaAPI | |
| let req = new Request(url) | |
| let apiResult = await req.loadString() | |
| apiResult = JSON.parse(apiResult) | |
| //console.log("1.: " + apiResult.statusCode) | |
| if (apiResult.statusCode == "403") { | |
| let loginMessage = "Login nicht möglich. Zugangsdaten prüfen." | |
| throw new Error(loginMessage); | |
| } else { | |
| gigyaCookieValue = apiResult.sessionInfo.cookieValue | |
| gigyaPersonID = apiResult.data.personId | |
| Keychain.set('gigyaCookieValue', gigyaCookieValue) | |
| Keychain.set('gigyaPersonID', gigyaPersonID) | |
| //console.log('gigyaCookieValue (new generated): ' + gigyaCookieValue) | |
| //console.log('gigyaPersonID (new generated): ' + gigyaPersonID) | |
| } | |
| } | |
| // 2. fetch JWT data from gigya | |
| // renew gigyaJWTToken once a day | |
| if (Keychain.contains('lastJWTCall') == false) { | |
| Keychain.set('lastJWTCall', 'never') | |
| } | |
| let gigyaJWTToken | |
| if (Keychain.contains('gigyaJWTToken')) { | |
| gigyaJWTToken = Keychain.get('gigyaJWTToken') | |
| } | |
| //console.log('gigyaJWTToken (from keychain): ' + gigyaJWTToken) | |
| if (gigyaJWTToken == "" || typeof(gigyaJWTToken) == "undefined") { | |
| let expiration = 87000 | |
| url = gigyaURL + '/accounts.getJWT?oauth_token=' + gigyaCookieValue + '&login_token=' + gigyaCookieValue + '&expiration=' + expiration + '&fields=data.personId,data.gigyaDataCenter&ApiKey=' + gigyaAPI | |
| req = new Request(url) | |
| apiResult = await req.loadString() | |
| apiResult = JSON.parse(apiResult) | |
| //console.log("3.: " + apiResult.statusCode) | |
| gigyaJWTToken = apiResult.id_token | |
| Keychain.set('gigyaJWTToken', gigyaJWTToken) | |
| //console.log('gigyaJWTToken (new generated): ' + gigyaJWTToken) | |
| const callDate = new Date().toJSON().slice(0, 13).replace(/-/g, '').replace(/T/g, '-') | |
| Keychain.set('lastJWTCall', callDate) | |
| //console.log('lastJWTCall (new generated): ' + callDate) | |
| } | |
| // 3. fetch data from kamereon (person) | |
| // if not in Keychain (we try to avoid quota limits here) | |
| let account_id | |
| if (Keychain.contains('account_id')) { | |
| account_id = Keychain.get('account_id') | |
| } | |
| //console.log('account_id (from keychain): ' + account_id) | |
| if (account_id == "" || typeof(account_id) == "undefined") { | |
| url = kamareonURL + '/commerce/v1/persons/' + gigyaPersonID + '?country=DE' | |
| req = new Request(url) | |
| req.method = "GET" | |
| req.headers = { "x-gigya-id_token": gigyaJWTToken, "apikey": kamareonAPI } | |
| apiResult = await req.loadString() | |
| //console.log("4.: " + apiResult) | |
| apiResult = JSON.parse(apiResult) | |
| if (apiResult.type == "FUNCTIONAL") { | |
| let quotaMessage = apiResult.messages[0].message + " – Login derzeit nicht möglich. Später nochmal versuchen." | |
| throw new Error(quotaMessage); | |
| } else { | |
| account_id = apiResult.accounts[accountNumber].accountId | |
| Keychain.set('account_id', account_id) | |
| //console.log('account_id (new generated): ' + account_id) | |
| } | |
| } | |
| // 4. fetch data from kamereon (all vehicles data) | |
| // we need this only once to get the picture of the car and the VIN! | |
| let carPicture | |
| if (Keychain.contains('carPicture')) { | |
| carPicture = Keychain.get('carPicture') | |
| } | |
| //console.log('carPicture (from keychain): ' + carPicture) | |
| if (Keychain.contains('VIN') && Keychain.get('VIN') != "") { | |
| VIN = Keychain.get('VIN') | |
| } | |
| //console.log('VIN (from keychain): ' + VIN) | |
| if (carPicture == "" || typeof(carPicture) == "undefined" || VIN == "" || typeof(VIN) == "undefined") { | |
| url = kamareonURL + '/commerce/v1/accounts/' + account_id + '/vehicles?country=DE' | |
| req = new Request(url) | |
| req.method = "GET" | |
| req.headers = { "x-gigya-id_token": gigyaJWTToken, "apikey": kamareonAPI } | |
| apiResult = await req.loadString() | |
| apiResult = JSON.parse(apiResult) | |
| if (carNumber == "") { // fallback | |
| carNumber = 0; | |
| } | |
| // set correct carNumber to array (starts with 0) | |
| carNumber = carNumber - 1; | |
| //console.log("carNumber: " + carNumber) | |
| // set carPicture | |
| carPicture = await apiResult.vehicleLinks[carNumber].vehicleDetails.assets[0].renditions[0].url | |
| Keychain.set('carPicture', carPicture) | |
| //console.log('carPicture (new): ' + carPicture) | |
| // set VIN | |
| if (VIN == "" || typeof(VIN) == "undefined") { | |
| VIN = apiResult.vehicleLinks[carNumber].vin | |
| Keychain.set('VIN', VIN) | |
| //console.log('VIN (new generated): ' + VIN) | |
| } | |
| } | |
| // log all vehicle data: | |
| // console.log(apiResult.vehicleLinks) | |
| // NOW WE CAN READ AND SET EVERYTHING INTO AN OBJECT: | |
| const allResults = {}; | |
| // real configurator picture of the vehicle | |
| // old call: let carPicture = allVehicleData.vehicleLinks[0].vehicleDetails.assets[0].renditions[0].url // renditions[0] = large // renditions[1] = small image | |
| allResults["carPicture"] = carPicture | |
| // batteryStatus | |
| // version: 2 | |
| // batteryLevel = Num (percentage) | |
| // plugStatus = bolean (0/1) | |
| // chargeStatus = bolean (0/1) (?) | |
| let batteryStatus = await getStatus('battery-status', 2, kamareonURL, account_id, VIN, gigyaJWTToken, kamareonAPI) | |
| allResults["batteryStatus"] = batteryStatus | |
| // cockpitStatus | |
| // version: 2 | |
| // totalMileage = Num (in Kilometres!) | |
| let cockpitStatus = await getStatus('cockpit', 1, kamareonURL, account_id, VIN, gigyaJWTToken, kamareonAPI) | |
| allResults["cockpitStatus"] = cockpitStatus | |
| // locationStatus | |
| // version: 1 | |
| // gpsLatitude | |
| // gpsLongitude | |
| // LastUpdateTime | |
| let locationStatus = await getStatus('location', 1, kamareonURL, account_id, VIN, gigyaJWTToken, kamareonAPI) | |
| allResults["locationStatus"] = locationStatus | |
| // chargeSchedule | |
| // note: unused at the moment! | |
| // version: 1 | |
| let chargeSchedule = await getStatus('charging-settings', 1, kamareonURL, account_id, VIN, gigyaJWTToken, kamareonAPI) | |
| allResults["chargeSchedule"] = chargeSchedule | |
| // hvacStatus | |
| // version: 1 | |
| let hvacStatus = await getStatus('hvac-status', 1, kamareonURL, account_id, VIN, gigyaJWTToken, kamareonAPI) | |
| allResults["hvacStatus"] = hvacStatus | |
| //console.log('hvacStatus: ' + hvacStatus) | |
| // query parameter / args | |
| // if query action = "start_ac" we start "vorklimatisierung" | |
| // default temperature will be 21°C | |
| let query_action = args.queryParameters.action | |
| if (query_action == "start_ac") { | |
| let attr_data = '{"data":{"type":"HvacStart","attributes":{"action":"start","targetTemperature":"21"}}}' | |
| let action = await postStatus('hvac-start', attr_data.toString(), 1, kamareonURL, account_id, VIN, gigyaJWTToken, kamareonAPI) | |
| //console.log("start_ac_action: " + action) | |
| //throw new Error(action) | |
| } | |
| if (query_action == "stop_ac") { | |
| let attr_data = '{"data":{"type":"HvacStart","attributes":{"action":"cancel"}}}' | |
| let action = await postStatus('hvac-start', attr_data.toString(), 1, kamareonURL, account_id, VIN, gigyaJWTToken, kamareonAPI) | |
| //console.log("stop_ac_action: " + action) | |
| } | |
| if (query_action == "start_charge") { | |
| let attr_data = '{"data":{"type":"ChargingStart","attributes":{"action":"start"}}}' | |
| let action = await postStatus('charging-start', attr_data.toString(), 1, kamareonURL, account_id, VIN, gigyaJWTToken, kamareonAPI) | |
| console.log("start_charge_action: " + action) | |
| } | |
| //console.log(allResults) | |
| // return array | |
| return allResults | |
| } | |
| // general function to get status-values from our vehicle | |
| async function getStatus(endpoint, version = 1, kamareonURL, account_id, VIN, gigyaJWTToken, kamareonAPI) { | |
| // fetch data from kamereon (single vehicle) | |
| url = kamareonURL + '/commerce/v1/accounts/' + account_id + '/kamereon/kca/car-adapter/v' + version + '/cars/' + VIN + '/' + endpoint + '?country=DE' | |
| req = new Request(url) | |
| req.method = "GET" | |
| req.headers = { "x-gigya-id_token": gigyaJWTToken, "apikey": kamareonAPI, "Content-type": "application/vnd.api+json" } | |
| apiResult = await req.loadString() | |
| if (req.response.statusCode == 200) { | |
| apiResult = JSON.parse(apiResult) | |
| } | |
| return apiResult.data | |
| console.log(apiResult.data) | |
| } | |
| // general function to POST status-values to our vehicle | |
| async function postStatus(endpoint, jsondata, version, kamareonURL, account_id, VIN, gigyaJWTToken, kamareonAPI) { | |
| url = kamareonURL + '/commerce/v1/accounts/' + account_id + '/kamereon/kca/car-adapter/v' + version + '/cars/' + VIN + '/actions/' + endpoint + '?country=DE' | |
| request = new Request(url) | |
| request.method = "POST" | |
| request.body = jsondata | |
| request.headers = { "x-gigya-id_token": gigyaJWTToken, "apikey": kamareonAPI, "Content-type": "application/vnd.api+json" } | |
| apiResult = await request.loadString() | |
| //console.log(apiResult) | |
| //debug: | |
| // throw new Error(url) | |
| let pushBody | |
| let sound | |
| if (request.response.statusCode == 200) { | |
| pushBody = "Die Übermittlung des Befehls war erfolgreich." | |
| sound = "piano_success" | |
| } else { | |
| pushBody = "Es ist ein Fehler beim Senden des Befehls aufgetreten. Keine Verbindung. Code:" + request.response.statusCode | |
| sound = "piano_error" | |
| } | |
| pushMessage = new Notification() | |
| pushMessage.identifier = "zoePostStatus" | |
| if (endpoint == "hvac-start") { | |
| pushMessage.title = "Kommando an Klimaanlage gesendet" | |
| } | |
| if (endpoint == "charge-start") { | |
| pushMessage.title = "Kommando an Ladeanlage gesendet" | |
| } | |
| //pushMessage.title = "Befehl gesendet" | |
| pushMessage.body = pushBody | |
| pushMessage.sound = sound | |
| //pushMessage.setTriggerDate(newDate); | |
| pushMessage.schedule() | |
| return apiResult | |
| } | |
| function time_convert(num) { | |
| var hours = Math.floor(num / 60); | |
| var minutes = num % 60; | |
| return hours + ":" + minutes; | |
| } | |
| // get images from local filestore or download them once | |
| // this part is inspired by the dm-toilet-paper widget | |
| // credits: https://gist.github.com/marco79cgn | |
| async function getImage(image, imgUrl) { | |
| let fm = FileManager.local() | |
| let dir = fm.documentsDirectory() | |
| let path = fm.joinPath(dir, image) | |
| if (fm.fileExists(path)) { | |
| return fm.readImage(path) | |
| //fm.remove(path) | |
| } else { | |
| // download once | |
| let imageUrl | |
| switch (image) { | |
| case 'my-renault-car-' + VIN + '.png': | |
| imageUrl = imgUrl | |
| break | |
| default: | |
| //console.log(`Sorry, couldn't find ${image}.`); | |
| } | |
| if (imageUrl) { | |
| let iconImage = await loadImage(imageUrl) | |
| fm.writeImage(path, iconImage) | |
| return iconImage | |
| } | |
| } | |
| } | |
| // helper function to download an image from a given url | |
| async function loadImage(imgUrl) { | |
| const req = new Request(imgUrl) | |
| return await req.loadImage() | |
| } | |
| // end of script |
@saxoyellow @ionutze Hi, at the moment Renault is not submitting the carPicture (Image of the Car) in the API. My code does not have a fallback for that. I will wait a bit if the Picture comes back to the API, and if not i will find a solution to implement a fallback. Will get back to you soon.
@ionutze @saxoyellow I think the image will come back soon by renault. Check your "my Renault" App and you will see that the app is also missing the image. Thats typically Renault :-)

@mountbatt good morning! Since yesterday evening everything is working again! thank you for your help 😊
@mountbatt I made a little update related to displaying kW during charge not always working: https://gist.github.com/dehsgr/32c4d3dd5f8125be3a4c66d04e41d9b8
I changed based on your latest version. Feel free to merge.
@mountbatt I made a little update related to displaying kW during charge not always working: https://gist.github.com/dehsgr/32c4d3dd5f8125be3a4c66d04e41d9b8
I changed based on your latest version. Feel free to merge.
Nice! Thanks! i updated it
Hi Mountbatt, what am i doing wrong, when I get the answer:
2025-09-10 15:16:27: Error on line 510:55: TypeError: undefined is not an object (evaluating 'apiResult.vehicleLinks[carNumber].vehicleDetails')
Thx for your Help 😋
Most likely Renault changed the API again…😞