Skip to content

Instantly share code, notes, and snippets.

@ghazlewood
Forked from mountbatt/ZOE-Widget.js
Last active July 1, 2021 20:05
Show Gist options
  • Save ghazlewood/8ec83b9bac3018a0309cde97411bd1b8 to your computer and use it in GitHub Desktop.
Save ghazlewood/8ec83b9bac3018a0309cde97411bd1b8 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: 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"
let language = "en" // 'de', 'en'
let distance_unit = "miles" // 'km', 'miles'
// translations
let ENLOCALE = {
"plugStateDisconnected": "Disconnected",
"plugStateConnected": "Connected",
"plugStateCharging": "Charging",
"plugStateStartCharge": "Start Charge",
"quotaLimit": "Quota Limit! – Renault limits the number of requests to their API. Wait a while and access should be resumed.",
"loginMessage": "Login failed. Check your credentials and try again.",
"batteryStatus": "Battery Status",
"rangeStatus": "Current Range",
"batteryTemperature": "Battery Temp.",
"avEnergyStatus": "Current Energy",
"odometer": "Odometer",
"location": "Location",
"openMap": "Open Map",
"myCar": "My+Car",
"mi": "Miles",
"km": "Kilometers",
"climateControl": "Climate Control",
"acStop": "Stop Conditioning",
"acStart": "Start Conditioning",
"hvacStart": "Preconditioning Requested",
"chargeStart": "Start Charging Requested",
"stopAC": "Stop AC",
"startAC": "Start AC",
"apiFailed": "Login currently not possible. Try again later.",
"commandSuccessful": "The command was successful.",
"commandFailed": "There was an error sending the command. No connection. Code: "
};
let DELOCALE = {
"plugStateDisconnected": "Entkoppelt",
"plugStateConnected": "Gekoppelt",
"plugStateCharging": "Wird geladen",
"plugStateStartCharge": "Laden startene",
"quotaLimit": "Quota Limit! – Datenabruf zur Zeit nicht möglich. Später nochmals versuchen oder bei Renault beschweren.",
"loginMessage": "Login failed. Check your credentials and try again.",
"batteryStatus": "Ladestand",
"rangeStatus": "Reichweite",
"batteryTemperature": "Akkutemp.",
"avEnergyStatus": "Verf. Energie",
"odometer": "Kilometerstand",
"location": "Position",
"openMap": "Karte öffnen",
"myCar": "Mein+Auto",
"mi": "Miles",
"km": "Kilometers",
"climateControl": "Vortemp.",
"stopAC": "Klima stoppen",
"startAC": "Klima starten",
"hvacStart": "Kommando an Klimaanlage gesendet",
"chargeStart": "Kommando an Ladeanlage gesendet",
"apiFailed": "Login derzeit nicht möglich. Später nochmal versuchen.",
"commandSuccessful": "Die Übermittlung des Befehls war erfolgreich.",
"commandFailed": "Es ist ein Fehler beim Senden des Befehls aufgetreten. Keine Verbindung. Code: "
};
// 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 = "⚫ " + translate(language, "plugStateDisconnected")
} else {
plugIcon = await getImage("zoe-plug-on.png", "")
plugStateLabel = "🟢 " + translate(language, "plugStateConnected")
}
if(data.batteryStatus.attributes.chargingStatus == "1.0"){
plugStateLabel = "⚡ " + translate(language, "plugStateCharging") + " …"
}
if(data.batteryStatus.attributes.plugStatus == 1 && data.batteryStatus.attributes.chargingStatus == "0"){
plugStateLabel = "➤ " + translate(language, "plugStateStartCharge")
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(translate(language, "quotaLimit"))
} else {
console.log(translate(language, "quotaLimit"))
}
}
if(typeof(data.batteryStatus) != 'undefined'){
let BatteryStack = column1.addStack()
BatteryStack.layoutVertically()
const batteryStatusLabel = BatteryStack.addText(translate(language, "batteryStatus"))
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(translate(language, "rangeStatus"))
RangeStatusLabel.font = Font.mediumSystemFont(12)
//const RangeStatusVal = RangeStack.addText(data.batteryStatus.attributes.batteryAutonomy.toString()+" km")
const RangeStatusVal = RangeStack.addText(convertKM2M(data.batteryStatus.attributes.batteryAutonomy))
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(translate(language, "batteryTemperature"))
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(translate(language, "avEnergyStatus"))
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(translate(language, "odometer"))
MileageStatusLabel.font = Font.mediumSystemFont(12)
let mileage = Math.round(data.cockpitStatus.attributes.totalMileage).toLocaleString()
//const MileageStatusVal = MileageStack.addText(mileage.toString()+" km")
const MileageStatusVal = MileageStack.addText(convertKM2M(mileage))
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(translate(language, "location"))
LocationLabel.font = Font.mediumSystemFont(12)
const LocationVal = LocationStack.addText("➤ " + translate(language, "openMap"))
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=" + translate(language, "myCar") + "&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(translate(language, "climateControl"))
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("➤ " + translate(language, "stopAC"))
ac_url = `scriptable:///run?scriptName=${scriptName}&action=stop_ac`;
} else {
AcVal = AcStack.addText("➤ " + translate(language, "startAC"))
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 = translate(language,"loginMessage")
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 + " – " + translate(language, "apiFailed")
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 = translate(language, "commandSuccessful")
sound = "piano_success"
} else {
pushBody = translate(language, "commandFailed") + request.response.statusCode
sound = "piano_error"
}
pushMessage = new Notification()
pushMessage.identifier = "zoePostStatus"
if(endpoint == "hvac-start"){
pushMessage.title = translate(language, "hvacStart")
}
if(endpoint == "charge-start"){
pushMessage.title = translate(language, "chargeStart")
}
//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()
}
function translate(language, string){
if (language.indexOf("de") > -1) {
return DELOCALE[string] ? DELOCALE[string] : string;
}
if (language.indexOf("en") > -1) {
return ENLOCALE[string] ? ENLOCALE[string] : string;
}
return string;
}
function convertKM2M(distance) {
if (distance_unit.indexOf("miles") > -1) {
distance = distance * 0.6
}
return parseFloat(distance).toFixed(0).toString() + " " + distance_unit;
}
// end of script
@ghazlewood
Copy link
Author

Added simple translation of strings to English

@ghazlewood
Copy link
Author

ghazlewood commented Jul 1, 2021

Intro

This widget shows the current status of the Renault ZOE. The login data for the My-Renault app are required for this. These must be entered in the script above.

Full credit to https://gist.github.com/mountbatt and also to https://gist.github.com/edddeduck for the English translation, installation instructions and KM to Miles changes - My input was purely adding a translation system and incorporating a function to calculate KM to Miles.

Requirements

  • iOS 14
  • Scriptable version 1.5 (or newer)
  • My RENAULT - Account access data (email + password)
  • Optional: Vehicle identification number (VIN / FIN) if there are several vehicles in the account
  • Mainly tested with a ZOE phase 2 (ZE50 2021), it does work with phase 1 (ZE20/40).
  • Has a few minor issues with car picture if you have two vehicles attached to the same account.
  • If you are not based in the UK I assume you know how to find the json file and update your gigyaAPI
  • You can set both your language and whether you want to see distances in km or miles.

Installation

  1. Copy the entire source code from above (click on "raw" at the top right)
  2. Open the Scriptable app (You can install this from the iOS AppStore Store)
  3. Click on the "+" symbol at the top right and paste the copied script
  4. Now change "your_email" and "your_pass" in the script at the top with the access data of your My-RENAULT account (e-mail address and associated password)
  5. Enter a 1 or 2 for your vehicle at ZOE_Phase
  6. Optionally, enter the VIN / FIN of your vehicle in the appropriate space. (This is only required if you have several vehicles in your account and want the widget to point to a specific vehicle or if there are problems retrieving data)
  7. Click on the title of the script at the top and give it a name (e.g. ZOE)
  8. Save the script by clicking on "Done" in the top left
  9. Go to your iOS home screen and long press anywhere to get into "Wiggle Mode" (which can also be used to arrange the app symbols)
  10. Press the "+" symbol at the top left, then scroll down to "Scriptable" (list is alphabetical), choose the second widget size (medium / wide format) and press "Add widget" at the bottom
  11. Press on the widget to edit its settings (optionally long press if the wiggle mode has already been exited)
  12. Under "Script" select the one created above (ZOE)
  13. Wait a moment (approx. 15 seconds) until the widget has loaded the data from the server.

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