Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
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: light-gray; icon-glyph: car;
// version 2022-07-01
// add your my-renault account data:
// let myRenaultUser = "user" // email
// let myRenaultPass = "pass" // password
let myRenaultUser = "demo@demo.de" // email
let myRenaultPass = "password123" // password
// set your ZOE Model (Phase 1 or 2) // bitte eingeben!
let ZOE_Phase = "2" // "1" or "2" // Nimm "2" bei einem Megane-E
// set your battery size in kWh // bitte eingeben!
let ZOE_Battery = "52" // "52" or "41" or "22" or "21" or "40" or "60" for Megane
// 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"
// do not edit
let kamareonURL = "https://api-wired-prod-1-euw1.wrd-aws.com"
let kamareonAPI = "VAX7XYKGfa92yMvXculCkEFyfZbuM7Ss"
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 == 0){
//plugIcon = await getImage("zoe-plug-off.png", "")
plugStateLabel = "⚫ Entkoppelt"
} else {
//plugIcon = await getImage("zoe-plug-on.png", "")
plugStateLabel = "🟢 Gekoppelt"
}
if(data.batteryStatus.attributes.chargingStatus == "1.0"){
plugStateLabel = "⚡ Wird geladen …"
}
if(data.batteryStatus.attributes.plugStatus == 1 && data.batteryStatus.attributes.chargingStatus == "0"){
plugStateLabel = "➤ 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("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("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("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("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("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("Position")
LocationLabel.font = Font.mediumSystemFont(12)
const LocationVal = LocationStack.addText("➤ Karte öffnen")
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=Mein+Auto&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("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("➤ Stoppen")
ac_url = `scriptable:///run?scriptName=${scriptName}&action=stop_ac`;
} else {
AcVal = AcStack.addText("➤ 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;
} else {
// 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', 2, 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)
}
// 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(api.Result.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
@jumpjack
Copy link

jumpjack commented Jun 15, 2022

About the PIN topic...
I found a datum in Renault servers response which looks like a PIN: you can find it into notification response:

data: Array(1)
0:
actionType: "COMMAND_RESPONSE"
commandResponse:
errorCategory:
categoryId: "00"
categoryType: "GENERIC_MSG"
status: "CANCELLED"
commandType: "HVAC_START"
kmrUserId: "yyyyyyy"               <<<<<<<<<===============  here
notifDate: "2022-06-15T15:36:27"
notificationId: "30371b21-fc0a-4fa6-9aab-5bda09cc11d3"
personId: "xxxxxxxxxxxxxxxx"
vin: "VFxxxxxxxxxxxxxxxx"

It's a 7 digits pin, "kmrUserId".

Maybe it is worth a try, but I don't know how/what to try...

This is my (buggy) method to query status of commands using notifications endpoint:

function manageNotification(type) {
	//https://api-wired-prod-1-euw1.wrd-aws.com/commerce/v1/persons/PPPPPPPPPPPPPPPPPPPP/notifications/kmr?country=IT&notificationId=d47296c9-6ab6-4192-9db1-4712010538fb
	notificationUrl = kamereonurl + "/commerce/v1/persons/" +
	personId.value +
	"/notifications/kmr" +
	"?apikey=" + KAMEREON_KEY +
	"&country=" + country.value;
	if (type === "last") {
		notificationUrl +=	"&notificationId=" + notificationId;
	} else {
		// raw url provides info on all previous notifications
	}

	notificationHeaders = {
		'apikey': KAMEREON_KEY,
        'x-gigya-id_token' :  JWT.value
	};
    notifRetry = 0;
	CommandCompleted = false;
	notifInterval = setInterval(getNotificationStatus, NOTIF_INT);
}


function getNotificationStatus(mex) {
console.log(">>>>>>",mex,notifRetry);
	notifRetry++;
	axios.get(
		notificationUrl,
		{
			headers: notificationHeaders
		}
	)
	.then(response => {
		pippo = response;
		if (response.data.length === 0) { // In case of empty response, if command has not yet executed,  retry some times
			if (!CommandCompleted) {
				if (notifRetry >= NOTIF_MAX) { // Give up after a given number of attempts
					if (notifInterval) clearInterval(notifInterval);
					console.log(notifRetry, notifInterval, "Given up.");
					output.value += "\nGiven up.\n";
				} else {
					// retry
					console.log("Attempt n. " + notifRetry +  ", please wait...");
					output.value += "\nAttempt n. " + notifRetry + ", please wait...\n";
				}
			} else { // Empty because completed ==> exit
                if (notifInterval) clearInterval(notifInterval);
                CommandCompleted = true;
				console.log("Already finished, exiting.");
			}
		} else {
			//console.log("POST result for notification n. " + notificationId + ": " + (JSON.stringify(response.data, null, 4)));
			try {
				if (mex === "last") {
					console.log(response.data[response.data.length-1].notifDate + ": " + response.data[response.data.length-1].commandType + ", " +  response.data[response.data.length-1].commandResponse.status);
					console.log(response);
					output.value += "\n" + response.data[response.data.length-1].notifDate + ": " + response.data[response.data.length-1].commandType + ", " +  response.data[response.data.length-1].commandResponse.status;
				} else {
					for (i=0; i < response.data.length; i++) {
						console.log(response.data[i].notifDate + ": " + response.data[i].commandType + ", " +  response.data[i].commandResponse.status);
					console.log(response);
						output.value += "\n" + response.data[i].notifDate + ": " + response.data[i].commandType + ", " +  response.data[i].commandResponse.status;
					}
				}
				if (response.data[response.data.length-1].commandResponse.status == "COMPLETED") {
                    if (notifInterval) clearInterval(notifInterval);
                    CommandCompleted = true;
					console.log("PROCESSING COMPLETED.");
                    output.value += "\nPROCESSING COMPLETED.";
				} else {
                    //CommandCompleted = false;
                    console.log("Waiting for command execution...");
                    output.value += "\nWaiting for command execution..."
				}
			} catch (e) {
				console.log("data error, raw response for attempt " + notifRetry + ":", response, e);
                output.value += "Data error, see console for details";
			}
		}
	})
	.catch(
	  function (error) {
		log.value += "\nERROR 005 for POST while managing notification n. " + notificationId + ":\n" + error + "\n";
		console.log("ERROR 005 for POST while  managing notification n. " + notificationId + ":\n" + error);
		console.log("POST ERROR 005:" ,error)
	  	login1.innerHTML = "ERR_POST";
		return ({ status: "POST ERROR 005 (Notifications)", data: response.data });
		}
	);
}

@mountbatt
Copy link
Author

mountbatt commented Jun 15, 2022

@jumpjack what do you want to archive? I can’t remember that we talked about a PIN?!

@jumpjack
Copy link

jumpjack commented Jun 15, 2022

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
Copy link

Tuedderbueddel commented Jun 19, 2022

Gab‘s ein Update vom Widget? Ich bekomme seit heute angehängte Fehlermeldung.
E4995026-83C7-43B3-AD87-197D577A811C

@mountbatt
Copy link
Author

mountbatt commented Jun 19, 2022

@Tuedderbueddel nein. Server ist nicht erreichbar. Siehe auch my Renault App…

@Tuedderbueddel
Copy link

Tuedderbueddel commented Jun 19, 2022

Ah, dann ist es klar. Danke!

@wopfel
Copy link

wopfel commented Jul 1, 2022

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;
			}

@mountbatt
Copy link
Author

mountbatt commented Jul 1, 2022

@wopfel Thanks … I updated the code …

@matzZz
Copy link

matzZz commented Aug 22, 2022

Hi, is there a solution to stop the charging at a specific % of charge?

@mountbatt
Copy link
Author

mountbatt commented Aug 22, 2022

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!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment