// 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 2021-02-10 | |
// add your my-renault account data: | |
let myRenaultUser = "user" // email | |
let myRenaultPass = "pass" // password | |
// set your ZOE Model (Phase 1 or 2) // bitte eingeben! | |
let ZOE_Phase = "2" // "1" or "2" | |
// should we use apple-maps or google maps? | |
let mapProvider = "apple" // "apple" or "google" | |
// optional: | |
// enter your VIN / FIN if you have more than 1 vehicle in your account | |
// or if you get any login-errors | |
// leave it blank to auto-select it | |
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 = "Ae9FDWugRxZQAGm3Sxgk7uJn6Q4CGEA2" | |
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() | |
//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.png", 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"){ | |
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 | |
} | |
chargingInstantaneousPower = Math.round(chargingInstantaneousPower).toLocaleString() | |
let chargingRemainingTime = time_convert(data.batteryStatus.attributes.chargingRemainingTime) | |
chargingRemainingTimeString = " | " + chargingRemainingTime + " h" | |
chargeStateLabel = +chargingInstantaneousPower +" 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("Verf. Energie") | |
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("Vortemp.") | |
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("➤ Klima stoppen") | |
ac_url = `scriptable:///run?scriptName=${scriptName}&action=stop_ac`; | |
} else { | |
AcVal = AcStack.addText("➤ Klima 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() | |
apiResult = JSON.parse(apiResult) | |
console.log("4.: " + 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[0].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) | |
// set carPicture | |
carPicture = await apiResult.vehicleLinks[0].vehicleDetails.assets[0].renditions[0].url | |
Keychain.set('carPicture', carPicture) | |
console.log('carPicture (new): ' + carPicture) | |
// set VIN | |
VIN = apiResult.vehicleLinks[0].vin | |
Keychain.set('VIN', VIN) | |
console.log('VIN (new generated): ' + VIN) | |
} | |
// 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 | |
} | |
// 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) | |
} 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 |
This comment has been minimized.
This comment has been minimized.
Thank you!! Really cool. To support passwords with (% sign), they should be escaped with let url = gigyaURL + '/accounts.login?loginID=' + encodeURIComponent(myRenaultUser) + '&password=' + encodeURIComponent(myRenaultPass) + '&apiKey=' + gigyaAPI |
This comment has been minimized.
This comment has been minimized.
Danke! Ist eingefügt |
This comment has been minimized.
This comment has been minimized.
Hi, ich habe eine ZOE von 2018. Die unterstützt leider nicht das Abfragen der location (bzw. das Abfragen schon, aber es kommt halt kein Ergebnis. Wenn man aber einfach den Code-Block in den Zeilen 112-122 in ein |
This comment has been minimized.
This comment has been minimized.
Danke @jrathert für den Hinweis. Ist nun bei allen Datenfeldern so eingebaut (Check if defined) und nun kann man es auch mit den älteren Modellen nutzen. |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
kWh ist eine Einheit für die Batteriekapazität (Energiemenge), nicht die Ladeleistung. Leistung wird in Watt bzw. kW angegeben. |
This comment has been minimized.
This comment has been minimized.
Danke @druelie – habe es korrigiert. Die falschen Werte allerdings kann ich derzeit nicht in den Griff kriegen, das ist das, was das Auto leider per API übermittelt. Weißt Du ob das in der Renault App richtig angezeigt wird? |
This comment has been minimized.
This comment has been minimized.
In der Renault App wird das ja gar nicht mehr angezeigt. Vermutlich auch aus diesem Grund. :-) |
This comment has been minimized.
This comment has been minimized.
Vielleicht lässt sich auch noch einiges von folgender Android App abschauen: Oder von diesem PHP-Script, was OS unabhängig funktioniert: Falls das noch nicht bekannt ist :) |
This comment has been minimized.
This comment has been minimized.
Es gibt einen (polnischen, aber dank Google Translate gut lesbaren) Artikel auf https://elektrowoz.pl/auta/renault-zoe-ze-50-teoretycznie-powinien-obslugiwac-moc-ladowania-70-kw-odkrycie-czytelnika/, in dem spekuliert wird, dass chargingInstantaneousPower der aktuell maximal moeglichen Ladeleistung entspricht. |
This comment has been minimized.
This comment has been minimized.
vielleicht lässt sich noch ein Parameter zum Script hinzufügen der einen wählen lässt, ob Google Maps oder Apple Maps bei der Lokalisierung verwendet werden soll |
This comment has been minimized.
This comment has been minimized.
@bloemi gute Idee! |
This comment has been minimized.
This comment has been minimized.
@bloemi Ist nun eingebaut … |
This comment has been minimized.
This comment has been minimized.
Vielen Dank für das geniale Skript. Etwas ähnliches hatte ich über ein paar Umwege realisiert.
|
This comment has been minimized.
This comment has been minimized.
Danke für das Skript! Leider zeigts bei mir immer die falsche Wagenfarbe und die Phase 1 an statt 2. Hatte vorher einen Phase 1 in meinem Konto hinterlegt, nachdem löschen aus der Renault App zeigt das Widget die Farbe aber trotzdem an. Habe auch die 43 mal aus kommentiert, Bild ändert sich aber nicht. |
This comment has been minimized.
This comment has been minimized.
Scriptable App neu installieren hats gelöst... |
This comment has been minimized.
This comment has been minimized.
@t-stricker Ja, das Bild vom Auto wird in einem Keychain Element gesichert. Also Lösung hättest Du Zeile 43 aktivieren können und nach 1 Tag wäre das dann erschienen. Oder Du hättest noch einfach |
This comment has been minimized.
This comment has been minimized.
Falls ihr noch Hilfe braucht Entwicklung/Testing gerne melden. |
This comment has been minimized.
This comment has been minimized.
Hi !! I always get the same error. Error on line 425:45: TypeError: undefined is not an object (evaluating 'apiResult.vehicleLinks[0].vehicleDetails') Any idea ? (I’m from France ) thank you very much ! |
This comment has been minimized.
This comment has been minimized.
@RYOSAEBAXYZ Are you sure, you copied the whole script? It looks like the script is not able to get all data from the server. |
This comment has been minimized.
This comment has been minimized.
Should I be changing the country code for the API ? (France ?)
Envoyé de mon téléphone mobile
… Le 14 nov. 2020 à 14:24, Tobias Battenberg ***@***.***> a écrit :
***@***.*** commented on this gist.
@RYOSAEBAXYZ Are you sure, you copied the whole script? It looks like the script is not able to get all data from the server.
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub, or unsubscribe.
|
This comment has been minimized.
This comment has been minimized.
Still the same msg no matter what. It seems credentials are not at fault. I can’t figure out what it is. |
This comment has been minimized.
This comment has been minimized.
Maybe its a better approach to ask for the users credentials and store them in the device keychain instead to safe cleartext data at the top of the script ? |
This comment has been minimized.
This comment has been minimized.
@RYOSAEBAXYZ I think i figured out why that's not working in france:
|
This comment has been minimized.
This comment has been minimized.
Thanks !! Where should your bit be added then ? |
This comment has been minimized.
This comment has been minimized.
You figured it out !! That was it !! The multiple accounts !! Thanks a lot ! |
This comment has been minimized.
This comment has been minimized.
Since today the call is resulting in 403 making Kameron server calls from Germany. Is this reproducible to someone else? |
This comment has been minimized.
This comment has been minimized.
same error here from cologne |
This comment has been minimized.
This comment has been minimized.
i hope this is only temporary |
This comment has been minimized.
This comment has been minimized.
Seems that Renault changed the Kameron API key. Change it according to my revision and it should work again. |
This comment has been minimized.
This comment has been minimized.
@dehsgr: Where did you extract the new API key? |
This comment has been minimized.
This comment has been minimized.
@db-EV you might intercept communication of your MyRenault App and sniff that key. |
This comment has been minimized.
This comment has been minimized.
@dehsgr: Then the app isn't getting the key from https://renault-wrd-prod-1-euw1-myrapp-one.s3-eu-west-1.amazonaws.com/configuration/android/config_en_GB.json (or other localized .json) anymore. Maybe it's getting the API key from Firebase now? |
This comment has been minimized.
This comment has been minimized.
@db-EV I really don‘t know where the key comes from. I sniffed it with traffic interception, following the request path of this gist. I did need a fast solution as I used same request collection directly from within node-red. If there is any possibility to get these keys dynamically, I would like you to tell me how. ;-) |
This comment has been minimized.
This comment has been minimized.
Thanks @dehsgr! Great! |
This comment has been minimized.
This comment has been minimized.
@dehsgr what did you use for the sniffing? I am trying to document the API on https://github.com/hacf-fr/renault-api/ (you can contact me on |
This comment has been minimized.
This comment has been minimized.
@epenet I used Fiddler and was able to track Kamareon requests too. :-D |
This comment has been minimized.
This comment has been minimized.
I'm still unable to track Kamereon requests (the Android application complains about the certificates) but luckily both Gigya and Firebase requests are decrypted correctly and @db-EV was correct regarding Firebase. I have created a pull request on hacf-fr/renault-api#184 (branch https://github.com/hacf-fr/renault-api/tree/firebase) if anyone wants to help... |
This comment has been minimized.
This comment has been minimized.
@epenet did you install & trust Fiddler root certificate on Android? This is pre-requisite. |
This comment has been minimized.
This comment has been minimized.
@dehsgr yes I did, and that's how I'm able to decrypt Gigya and Firebase data. Maybe it's because I'm stuck on version 4.5.0 of the application |
This comment has been minimized.
This comment has been minimized.
With the new API key, it worked exactly once. Now the data no longer updates (without an error message) even though I am on the road. I am at a loss. |
This comment has been minimized.
This comment has been minimized.
Maybe just "normal" problems with the Renault servers. Running here with 2 different accounts without problems so far. |
This comment has been minimized.
This comment has been minimized.
No problem with the new Ae9... key neither. Still interested in finding the original source for the API keys. |
This comment has been minimized.
This comment has been minimized.
@lix-src I am stuck on the same problem. It worked once but now the data does not change anymore - without any errors. |
This comment has been minimized.
This comment has been minimized.
@lix-src But the MyRenault App is not updating too! So I think we have to wait … |
This comment has been minimized.
This comment has been minimized.
@lix-srv & @mountbatt did you intercept app traffic and get the same key? Maybe you have to use another key. Or Renault is changing sth. in the backend again. |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
My last update was at 17:55, so for me the Ae9... key is still working |
This comment has been minimized.
This comment has been minimized.
There seem to be some issues indeed. My Renault app hasn’t refreshed since 3pm. :-( |
This comment has been minimized.
This comment has been minimized.
After a clean install, I can see the Renault app retrieving the apikey Ae9FDWugRxZQAGm3Sxgk7uJn6Q4CGEA2 (among others) from https://firebaseremoteconfig.googleapis.com/v1/projects/renault-brand-ios/namespaces/firebase:fetch?key=... |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
new error on line 119:85 ReferenceError: cannot access uninitialized variable New key? |
This comment has been minimized.
This comment has been minimized.
@coolio2004 still working here... |
This comment has been minimized.
This comment has been minimized.
Renault changed sth. on their API again. Just confusing. :-( |
This comment has been minimized.
This comment has been minimized.
Do have problems getting data? For me is the API still working... |
This comment has been minimized.
This comment has been minimized.
not for me in cologne |
This comment has been minimized.
This comment has been minimized.
On fetching user data I get code 403 now: This is caused on line 340 in above code. This results in "345:34: Type Error: undefined is not an object (evaluating 'apiResult.data.personId". But there seems to be another issue now |
This comment has been minimized.
This comment has been minimized.
@dehsgr Same problem here … will have a look. Hope we will find it :) – Zeddy is killed too … |
This comment has been minimized.
This comment has been minimized.
It maybe is the "2. fetch user data from gigya" which causes the problem. My ZoePHP-Script goes straight from step 1 to step 3. |
This comment has been minimized.
This comment has been minimized.
@mountbatt I'm a bit further. We can summarize first Lines of getData() function a bit:
This is what I was doing to get my personal API usage working from within node-red again. I saw that @db-EV at https://github.com/db-EV/ZoePHP/blob/main/src/index.php. But I can't get your code running completely with these changes. As I can see this shouldn't be an issue of using GET instead of POST requests here. But I'm missing sth. other?! |
This comment has been minimized.
This comment has been minimized.
Try this one on step 1:
|
This comment has been minimized.
This comment has been minimized.
@db-EV I think gigyaDataCenter is missing in your function. Or isn't it required anymore? |
This comment has been minimized.
This comment has been minimized.
I have no value there in my script. Maybe it's not exactly working the same in JS. But I see another problem in your solution: |
This comment has been minimized.
This comment has been minimized.
More exactly: you never need the gigyaDataCenter value, you just grab the gigyaPersonID to send it along with your request to the Renault Kamereon server to get the AccountID. |
This comment has been minimized.
This comment has been minimized.
I just edited my fork. This isn't completely working for now, but we can fetch all required data. Line 121 is producing some error: |
This comment has been minimized.
This comment has been minimized.
@db-EV you're right. Thanks for that hint! I'll change that in my code too. |
This comment has been minimized.
This comment has been minimized.
The ' sign seems to be wrong in the line 121. It's looks more like a french '. I hope you understand what I mean... |
This comment has been minimized.
This comment has been minimized.
@db-EV the '-sign is correct there. I got the script fully functional now. @mountbatt you might take a look at my changes. Thanks for support. |
This comment has been minimized.
This comment has been minimized.
Dear readers/users please update to latest version 2021-02-09. Give thanks to @dehsgr |
This comment has been minimized.
This comment has been minimized.
thanx for your hard work |
This comment has been minimized.
This comment has been minimized.
@mountbatt I just re-fixed charging link, if you would like to merge... |
This comment has been minimized.
This comment has been minimized.
@dehsgr thanks. Merged! |
This comment has been minimized.
Intro
Dieses Widget zeigt den aktuellen Status des Renault ZOE an. Dazu werden die Login-Daten für die My-Renault App benötigt. Diese müssen oben im Script eingegeben werden.
Anforderungen
Installation
Wähle unter "Script" das oben erstellte aus (ZOE)
Bekannte Fehler
Disclaimer
Bei diesem Widget / Script handelt es sich um ein reines Privatprojekt ohne kommerzielle Hintergründe etc. Ich stehe weder in Beziehung zu Renault, noch bekomme ich etwas dafür. Die API-Anbindung zu den Renault-Servern kann jederzeit von Renault geändert und der Zugriff des Widgets somit unmöglich werden. Beachte unbedingt, dass Du dein My-Renault-Passwort oben im Script nicht mit anderen teilst!
Hinweis
Beachte, dass zu viele Zugriffe auf die My-RENAULT API in kurzer Zeit zu einer temporären Abrufsperre führen. Das gilt aber für alle Zugriffe wie z.B. via Zeddy usw. Nach kurzer Zeit sollte der Abruf wieder gelingen.
Danke
Ein Dank geht an marco79cgn für sein dm-Widget – dies hat mich zu diesem Projekt inspiriert. Zudem gebührt Respekt muskat, der die API von Renault intensiv durchforstet hat.
Updates
10.02.2021 10:24 Uhr
09.02.2021 02:10 Uhr
02.02.2021 01:30 Uhr
31.10.2020 12:00 Uhr
26.10.2020 16:58 Uhr
26.10.2020 16:22 Uhr
26.10.2020 10:48 Uhr
26.10.2020 9:35 Uhr
Roadmap