-
-
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 2024-04-19 | |
// latest changes: | |
// new kameron api key | |
// added language strings so you can translate it by yourself! | |
// add your my-renault account data: | |
// let myRenaultUser = "user" // email | |
// let myRenaultPass = "pass" // password | |
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 + " 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 |
There is an ongoing investigation to discover how to "crack" the Renault/Nissan SRP authentication, apparently required to run some endpoints involving door locking, windows management, engine starting and other things, all replying with "not authorized - error".
@Tuedderbueddel nein. Server ist nicht erreichbar. Siehe auch my Renault App…
Ah, dann ist es klar. Danke!
When carNumber
is an empty string, it is set to 0. In the next few code lines, the variable is decremented by 1, so it gets -1 then. A negative array index would be used then.
https://gist.github.com/mountbatt/772e4512089802a2aa2622058dd1ded7#file-zoe-widget-js-L449
Maybe this would fix it?
if(carNumber == ""){ // fallback
carNumber = 0;
} else {
// set correct carNumber to array (starts with 0)
carNumber = carNumber - 1;
}
@wopfel Thanks … I updated the code …
Hi, is there a solution to stop the charging at a specific % of charge?
Hi, is there a solution to stop the charging at a specific % of charge?
Not with this widget! This widget only runs while you open it. And your iPhone decides when to refresh it. So no background activity / Monitoring etc is possible. This behavior is by design from Apple!
Hi Mountbatt.
This is an awesome script. Many many thanks for taking the time to code it. I guess you’re a Zoe owner yourself!
My only suggestion would be to modify the code so that the language to be displayed is not hard coded in to the script but available to easily modify at the top with the rest of the user defined variables.
Thank you again in any case!
Hi Mountbatt,
Thanks for the script, I have been using it successfully and frequently since almost the beginning.
FYI, I am currently getting some off values for the remaining charge. The attached result for a 52kWh ZOE. As far as I can tell from the log, the 13kWh is reported by the renault server.
Is there any idea when there will be an update about which complaints are there from renault, so that other people could avoid them?
And is it possible to work again on the script beside this complaints (e.g. if they are about the api key which doesn't need to be provided).
Is there any idea when there will be an update about which complaints are there from renault, so that other people could avoid them?
And is it possible to work again on the script beside this complaints (e.g. if they are about the api key which doesn't need to be provided).
Dear @duracell , the latest version is still in the "reviews" section and it still works. But i will bring it back for all of you here.
Script is back! Enjoy … But there are no significant code changes since a few month, cause it still works, if Renaults servers are up and reachable!
new: Langauge strings - you can now translate it by yourself by changing layout strings - enjoy, @codex-20
Great :)
I knew that is available there, but some might not and I was not sure if it's a good idea to mention this if someone has a problem with the script. But good to hear that it's back and hopefully receive updates if renault changes anything.
Can you tell what renault wanted?
Any know if Renault have just changed the Kamereon API key again? ... I have been getting authentication errors ("access_denied, "Unauthorized") .
It started (or stopped) suddenly this morning around 9am. I can still log in on Renault App, so I dont think account itself has expired.
Regards
Kevin
UPDATE: Many thanks epenet for quick response :-)
See hacf-fr/renault-api#848
New key: YjkKtHmGfaceeuExUDKGxrLZGGvtVS0J
@epenet thanks a lot!!!!
app updated on 26/2, probably new apikey, hence all 3rd party apps broken.
No, ZoePHP and HA still working.
For the cockpit information / mileage, see hacf-fr/renault-api#1145
@ionutze try to set your carNumber in code
Energy in kWh no longer displays it.
Actually not transmitted by Renault API. We will see if batteryAvailableEnergy is getting available again.
@jumpjack what do you want to archive? I can’t remember that we talked about a PIN?!