-
-
Save tobwil/94ed7a35ebf8f51c40bbb38f1cc74c77 to your computer and use it in GitHub Desktop.
Scriptable iOS widget that displays the status of your Renault ZOE on your iPhone and iPad.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Variables used by Scriptable. | |
// These must be at the very top of the file. Do not edit. | |
// icon-color: yellow; icon-glyph: magic; | |
// add your my-renault account data: | |
let myRenaultUser = "your_email" // email | |
let myRenaultPass = "your_pass" // password | |
// optional: enter your VIN / FIN if you have more than 1 vehicle in your account | |
// leave it blank to auto-select it | |
let VIN = "" // starts with VF1... | |
// do not edit | |
let kamareonURL = "https://api-wired-prod-1-euw1.wrd-aws.com" | |
let kamareonAPI = "oF09WnKqvBDcrQzcW1rJNpjIuy7KdGaB" | |
let gigyaURL = "https://accounts.eu1.gigya.com" | |
let gigyaAPI = "3_7PLksOyBRkHv126x5WhHb-5pqC1qFR8pQjxSeLB6nhAnPERTUlwnYoznHSxwX668" | |
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() | |
/* uncomment for local tests (dev mode) */ | |
// const data = {"carPicture":"https://3dv2.renault.com/ImageFromBookmark?configuration=DLIGM2%2FKITPOU%2FDANGMO%2FITPK4%2FVOLCHA%2FREACTI%2FAEBS07%2FPRAHL%2FRRCAM%2FX10%2FB10%2FEA3%2FDG%2FCAREG%2FVSTLAR%2FRET03%2FPROJAB%2FRALU17%2FDRAP13%2F3ATRPH%2FTEKPN%2FALEVA%2FVLCUI2%2FRETRCR%2FRETC%2FLVAREL%2FSGACHA%2FNA419%2FRNORM%2FTL09A%2FNBT022&databaseId=a864e752-b1b9-405e-9c3e-880073e36cc9&bookmarkSet=RSITE&bookmark=EXT_34_DESSUS&profile=HELIOS_OWNERSERVICES_LARGE","batteryStatus":{"type":"Car","id":"VF1AG000XXXXXXXX","attributes":{"timestamp":"2020-10-25T12:41:33Z","batteryLevel":66,"batteryTemperature":20,"batteryAutonomy":218,"batteryCapacity":0,"batteryAvailableEnergy":34,"plugStatus":1,"chargingStatus":1.0,"chargingRemainingTime":10,"chargingInstantaneousPower":44.2}},"cockpitStatus":{"type":"Car","id":"VF1AG000XXXXXXXX","attributes":{"fuelAutonomy":0,"fuelQuantity":0,"totalMileage":21783.5}},"locationStatus":{"type":"Car","id":"VF1AG000XXXXXXXX","attributes":{"gpsLatitude":50.9270183333333,"gpsLongitude":7.03316944444444,"lastUpdateTime":"2020-10-25T12:39:22Z"}},"chargeSchedule":{"type":"Car","id":"VF1AG000XXXXXXXX","attributes":{"mode":"always","schedules":[{"id":1,"activated":false},{"id":2,"activated":false},{"id":3,"activated":false},{"id":4,"activated":false},{"id":5,"activated":false}]}}} | |
// phase 1 test | |
// const data = {"carPicture":"https://3dv2.renault.com/ImageFromBookmark?configuration=DLIGM2%2FKITPOU%2FDANGMO%2FITPK4%2FVOLCHA%2FREACTI%2FAEBS07%2FPRAHL%2FRRCAM%2FX10%2FB10%2FEA3%2FDG%2FCAREG%2FVSTLAR%2FRET03%2FPROJAB%2FRALU17%2FDRAP13%2F3ATRPH%2FTEKPN%2FALEVA%2FVLCUI2%2FRETRCR%2FRETC%2FLVAREL%2FSGACHA%2FNA419%2FRNORM%2FTL09A%2FNBT022&databaseId=a864e752-b1b9-405e-9c3e-880073e36cc9&bookmarkSet=RSITE&bookmark=EXT_34_DESSUS&profile=HELIOS_OWNERSERVICES_LARGE","batteryStatus":{"type":"Car","id":"VF1AG000XXXXXXXX","attributes":{"timestamp":"2020-10-25T12:41:33Z","batteryLevel":66,"batteryTemperature":20,"batteryAutonomy":218,"batteryCapacity":0,"batteryAvailableEnergy":34,"plugStatus":1,"chargingStatus":1.0,"chargingRemainingTime":10,"chargingInstantaneousPower":1300.2}},"cockpitStatusX":{"type":"Car","id":"VF1AG000XXXXXXXX","attributes":{"fuelAutonomy":0,"fuelQuantity":0,"totalMileage":21783.5}},"locationStatusX":{"type":"Car","id":"VF1AG000XXXXXXXX","attributes":{"gpsLatitude":50.9270183333333,"gpsLongitude":7.03316944444444,"lastUpdateTime":"2020-10-25T12:39:22Z"}},"chargeSchedule":{"type":"Car","id":"VF1AG000XXXXXXXX","attributes":{"mode":"always","schedules":[{"id":1,"activated":false},{"id":2,"activated":false},{"id":3,"activated":false},{"id":4,"activated":false},{"id":5,"activated":false}]}}} | |
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.spacing = 15 | |
const column0 = wrap.addStack() | |
column0.layoutVertically() | |
if(data.carPicture){ | |
const icon = await getImage("my-renault-car.png", data.carPicture) | |
let CarStack = column0.addStack() | |
let iconImg = CarStack.addImage(icon) | |
} | |
column0.addSpacer(8) | |
if(typeof(data.batteryStatus) !== 'undefined'){ | |
let plugIcon | |
let plugStateLabel | |
const PlugWrap = column0.addStack() | |
PlugWrap.layoutHorizontally() | |
//PlugWrap.setPadding(0,15,0,15) | |
if(data.batteryStatus.attributes.plugStatus == 0){ | |
plugIcon = await getImage("zoe-plug-off.png", "") | |
plugStateLabel = "⚫ Entkoppelt" | |
} else { | |
plugIcon = await getImage("zoe-plug-on.png", "") | |
plugStateLabel = "🟢 Gekoppelt" | |
} | |
const PlugText = PlugWrap.addStack() | |
PlugText.setPadding(0,10,0,0) | |
PlugText.layoutVertically() | |
plugStateLabel = PlugText.addText(plugStateLabel) | |
plugStateLabel.font = Font.regularSystemFont(12) | |
PlugText.addSpacer(4) | |
if(data.batteryStatus.attributes.chargingStatus == "1.0"){ | |
let chargingInstantaneousPower = data.batteryStatus.attributes.chargingInstantaneousPower | |
chargingInstantaneousPower = Math.round(chargingInstantaneousPower) | |
// check if the numbers are in watt or kwh | |
if(chargingInstantaneousPower > 150){ | |
// if over 200, we believe the value is in watt :-) | |
chargingInstantaneousPower = chargingInstantaneousPower / 1000 | |
} | |
chargingInstantaneousPower = Math.round(chargingInstantaneousPower).toLocaleString() | |
chargeStateLabel = "⚡ Lädt ("+ chargingInstantaneousPower +" kWh)" | |
chargeStateLabel = PlugText.addText(chargeStateLabel) | |
chargeStateLabel.font = Font.regularSystemFont(12) | |
} | |
} | |
const column1 = wrap.addStack() | |
column1.layoutVertically() | |
column1.addSpacer(10) | |
if(typeof(data.batteryStatus) !== 'undefined'){ | |
let BatteryStack = column1.addStack() | |
BatteryStack.layoutVertically() | |
const batteryStatusLabel = BatteryStack.addText("Ladestand") | |
batteryStatusLabel.font = Font.mediumSystemFont(13) | |
const batteryStatusVal = BatteryStack.addText(data.batteryStatus.attributes.batteryLevel.toString()+" %") | |
batteryStatusVal.font = Font.boldSystemFont(20) | |
column1.addSpacer(21) | |
} | |
if(typeof(data.batteryStatus) !== 'undefined'){ | |
let RangeStack = column1.addStack() | |
RangeStack.layoutVertically() | |
const RangeStatusLabel = RangeStack.addText("Reichweite") | |
RangeStatusLabel.font = Font.mediumSystemFont(13) | |
const RangeStatusVal = RangeStack.addText(data.batteryStatus.attributes.batteryAutonomy.toString()+" km") | |
RangeStatusVal.font = Font.boldSystemFont(20) | |
} | |
const column2 = wrap.addStack() | |
column2.layoutVertically() | |
column2.addSpacer(10) | |
if(typeof(data.cockpitStatus) !== 'undefined'){ | |
let MileageStack = column2.addStack() | |
MileageStack.layoutVertically() | |
const MileageStatusLabel = MileageStack.addText("Kilometerstand") | |
MileageStatusLabel.font = Font.mediumSystemFont(13) | |
let mileage = Math.round(data.cockpitStatus.attributes.totalMileage).toLocaleString() | |
const MileageStatusVal = MileageStack.addText(mileage.toString()+" km") | |
MileageStatusVal.font = Font.boldSystemFont(20) | |
column2.addSpacer(21) | |
} | |
if(typeof(data.locationStatus) !== 'undefined'){ | |
let LocationStack = column2.addStack() | |
LocationStack.spacing = 6 | |
LocationStack.layoutVertically() | |
const LocationLabel = LocationStack.addText("Position") | |
LocationLabel.font = Font.mediumSystemFont(12) | |
//column2.addSpacer(0) | |
const LocationVal = LocationStack.addText("➤ Karte öffnen") | |
LocationVal.font = Font.boldSystemFont(12) | |
LocationVal.url = "http://maps.apple.com/?q=Mein+Auto&ll="+data.locationStatus.attributes.gpsLatitude+","+data.locationStatus.attributes.gpsLongitude // http://maps.apple.com/?ll=50.894967,4.341626 | |
} else { | |
//column2.addText(" test ") | |
} | |
// KM_PER_MILE = 1.609344 | |
} | |
// 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 from gigya | |
let url = gigyaURL + '/accounts.login?loginID=' + encodeURIComponent(myRenaultUser) + '&password=' + encodeURIComponent(myRenaultPass) + '&apiKey=' + gigyaAPI | |
let req = new Request(url) | |
let apiResult = await req.loadString() | |
apiResult = JSON.parse(apiResult) | |
let gigyaCookieValue = apiResult.sessionInfo.cookieValue | |
// 2. fetch user data from gigya | |
url = gigyaURL + '/accounts.getAccountInfo?oauth_token=' + gigyaCookieValue | |
req = new Request(url) | |
apiResult = await req.loadString() | |
apiResult = JSON.parse(apiResult) | |
let gigyaPersonID = apiResult.data.personId | |
let gigyaGigyaDataCenter = apiResult.data.gigyaDataCenter | |
// 3. fetch JWT data from gigya | |
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) | |
let gigyaJWTToken = apiResult.id_token | |
// 4. fetch data from kamereon (person) | |
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() | |
apiResult = JSON.parse(apiResult) | |
let account_id = apiResult.accounts[0].accountId | |
//console.log(apiResult) | |
// 5. fetch data from kamereon (all vehicles data) | |
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) | |
let allVehicleData = apiResult | |
//console.log(allVehicleData) | |
// check if credentials contain a VIN, if not, grab the first one from result array | |
if(!VIN){ | |
VIN = apiResult.vehicleLinks[0].vin | |
} | |
// NOW WE CAN ACCESS EVERYTHING: | |
const allResults = {}; | |
// real configurator picture of the vehicle | |
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 | |
//console.log(batteryStatus) | |
// cockpitStatus | |
// version: 2 | |
// totalMileage = Num (in Kilometres!) | |
let cockpitStatus = await getStatus('cockpit', 2, kamareonURL, account_id, VIN, gigyaJWTToken, kamareonAPI) | |
allResults["cockpitStatus"] = cockpitStatus | |
//console.log(cockpitStatus) | |
// locationStatus | |
// version: 1 | |
// gpsLatitude | |
// gpsLongitude | |
// LastUpdateTime | |
let locationStatus = await getStatus('location', 1, kamareonURL, account_id, VIN, gigyaJWTToken, kamareonAPI) | |
allResults["locationStatus"] = locationStatus | |
//console.log(locationStatus) | |
// chargeStatus | |
// version: 1 | |
let chargeSchedule = await getStatus('charging-settings', 1, kamareonURL, account_id, VIN, gigyaJWTToken, kamareonAPI) | |
allResults["chargeSchedule"] = chargeSchedule | |
//console.log(chargeSchedule) | |
// return array | |
return allResults | |
// console.log(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 | |
} | |
// 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) | |
} else { | |
// download once | |
let imageUrl | |
switch (image) { | |
case 'my-renault-car.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 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment