Skip to content

Instantly share code, notes, and snippets.

@tobwil
Forked from mountbatt/ZOE-Widget.js
Created October 27, 2020 08:46
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tobwil/94ed7a35ebf8f51c40bbb38f1cc74c77 to your computer and use it in GitHub Desktop.
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.
// 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