Skip to content

Instantly share code, notes, and snippets.

@edddeduck
Forked from mountbatt/ZOE-Widget.js
Last active March 11, 2022 09:56
Show Gist options
  • Save edddeduck/d7bbbefdae65204c64a081dc8b517cac to your computer and use it in GitHub Desktop.
Save edddeduck/d7bbbefdae65204c64a081dc8b517cac 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;
// 15th November
// Corrected KM to Miles accuracy
// Added extra details about Gigya API code for different countries
// Added battery temp to ZE50 cars
// Fixed a few German errors into English
// add your my-renault account data:
let myRenaultUser = "" // email
let myRenaultPass = "" // password
// set your ZOE Model (Phase 1 or 2) // ZE50=2 ZE20/40=1
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 = "oF09WnKqvBDcrQzcW1rJNpjIuy7KdGaB"
let gigyaURL = "https://accounts.eu1.gigya.com"
let gigyaAPI = "3_e8d4g4SE_Fo8ahyHwwP7ohLGZ79HKNN2T8NjQqoNnk6Epj6ilyYwKdHUyCw3wuxz" // This is the UK code
// You might need to update the two API keys: one for Kamereon and one for Gigya. However in most cases the above codes
// should work.
//
// Both can be obtained from Renault; they're the same for everyone and shouldn't be confused with your API credentials.
//
// Find them at e.g.
//
// https://renault-wrd-prod-1-euw1-myrapp-one.s3-eu-west-1.amazonaws.com/configuration/android/config_en_GB.json
//
// Look for the section called gigyaProd
//
//(replacing en_GB with your locale for example fr_FR or it_IT
//
//IT 3_4LKbCcMMcvjDm3X89LU4z4mNKYKdl_W0oD9w-Jvih21WqgJKtFZAnb9YdUgWT9_a
//
// Details thanks to Pyze project on Github
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(Keychain.contains('gigyaGigyaDataCenter')) { Keychain.remove('gigyaGigyaDataCenter') }
}
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
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 = "⚫ Unplugged"
} else {
plugIcon = await getImage("zoe-plug-on.png", "")
plugStateLabel = "🟢 Plugged in"
}
if(data.batteryStatus.attributes.chargingStatus == "1.0"){
plugStateLabel = "⚡ Charging …"
}
if(data.batteryStatus.attributes.plugStatus == 1 && data.batteryStatus.attributes.chargingStatus == "0"){
plugStateLabel = "➤ Start Charge"
plugStateLabel.url = `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)
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! – Renault limit the number of requests. Wait a while and access should be resumed.')
} else {
console.log('Quota Limit! – Renault limit the number of requests. Wait a while and access should be resumed.')
}
}
if(typeof(data.batteryStatus) != 'undefined'){
let BatteryStack = column1.addStack()
BatteryStack.layoutVertically()
const batteryStatusLabel = BatteryStack.addText("Batt. Status")
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("Current Range")
RangeStatusLabel.font = Font.mediumSystemFont(12)
///////////////////////////////////
// Convert KM to Miles
let RangeStatusKM2M = data.batteryStatus.attributes.batteryAutonomy
RangeStatusKM2M = (RangeStatusKM2M * 0.6213712)
RangeStatusKM2M = Math.round(RangeStatusKM2M)
const RangeStatusVal = RangeStack.addText(RangeStatusKM2M.toString()+" miles")
//const RangeStatusVal = RangeStack.addText(data.batteryStatus.attributes.batteryAutonomy.toString()+" km")
// END Convert KM to Miles
////////////////////////////////////
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("Battery Temp.")
TempStatusLabel.font = Font.mediumSystemFont(12)
const TempStatusVal = TempStack.addText(data.batteryStatus.attributes.batteryTemperature.toString()+" °C")
TempStatusVal.font = Font.boldSystemFont(16)
}
}
// Disabled the Available Energy for Battery Temp instead
/*
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("Avail. Energy")
AvEnergyStatusLabel.font = Font.mediumSystemFont(12)
const AvEnergyStatusVal = AvEnergyStack.addText(data.batteryStatus.attributes.batteryAvailableEnergy.toString()+" kWh")
AvEnergyStatusVal.font = Font.boldSystemFont(16)
}
}
*/
// Add battery temp for ZE50 cars
if(ZOE_Phase == 2 && typeof(data.batteryStatus) != 'undefined'){
if(typeof(data.batteryStatus.attributes.batteryTemperature) != 'undefined'){
let TempStack = column1.addStack()
TempStack.layoutVertically()
const TempStatusLabel = TempStack.addText("Battery Temp.")
TempStatusLabel.font = Font.mediumSystemFont(12)
const TempStatusVal = TempStack.addText(data.batteryStatus.attributes.batteryTemperature.toString()+" °C")
TempStatusVal.font = Font.boldSystemFont(16)
}
}
// End of new Battery temp code
const column2 = wrap.addStack()
column2.layoutVertically()
//column2.addSpacer(3)
if(typeof(data.cockpitStatus) != 'undefined'){
let MileageStack = column2.addStack()
MileageStack.layoutVertically()
const MileageStatusLabel = MileageStack.addText("Odometer")
MileageStatusLabel.font = Font.mediumSystemFont(12)
///////////////////////////////////
// Convert KM to Miles
let milageKM2M = data.cockpitStatus.attributes.totalMileage
milageKM2M = (milageKM2M * 0.6213712)
milageKM2M = Math.round(milageKM2M)
const MileageStatusVal = MileageStack.addText(milageKM2M.toString()+" miles")
// END Convert KM to Miles
////////////////////////////////////
//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("Location")
LocationLabel.font = Font.mediumSystemFont(12)
const LocationVal = LocationStack.addText("➤ Open Map")
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("Climate control")
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("➤ Stop Conditioning")
ac_url = `scriptable:///run?scriptName=${scriptName}&action=stop_ac`;
} else {
AcVal = AcStack.addText("➤ Start Conditioning")
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 from gigya
let gigyaCookieValue
if(Keychain.contains('gigyaCookieValue') && Keychain.get('gigyaCookieValue') != ""){
gigyaCookieValue = Keychain.get('gigyaCookieValue')
}
console.log('gigyaCookieValue (from keychain): ' + gigyaCookieValue)
if(gigyaCookieValue == "" || typeof(gigyaCookieValue) == "undefined"){
let url = gigyaURL + '/accounts.login?loginID=' + encodeURIComponent(myRenaultUser) + '&password=' + encodeURIComponent(myRenaultPass) + '&apiKey=' + gigyaAPI
let req = new Request(url)
let apiResult = await req.loadString()
apiResult = JSON.parse(apiResult)
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
Keychain.set('gigyaCookieValue', gigyaCookieValue)
console.log('gigyaCookieValue (new generated): ' + gigyaCookieValue)
}
}
// 2. fetch user data from gigya
let gigyaPersonID
let gigyaGigyaDataCenter
if(Keychain.contains('gigyaPersonID') && Keychain.get('gigyaPersonID') != ""){
gigyaPersonID = Keychain.get('gigyaPersonID')
}
console.log('gigyaPersonID (from keychain): ' + gigyaPersonID)
if(Keychain.contains('gigyaGigyaDataCenter') && Keychain.get('gigyaGigyaDataCenter') != ""){
gigyaGigyaDataCenter = Keychain.get('gigyaGigyaDataCenter')
}
console.log('gigyaGigyaDataCenter (from keychain): ' + gigyaGigyaDataCenter)
if(gigyaPersonID == "" || gigyaGigyaDataCenter == "" || typeof(gigyaPersonID) == "undefined" || typeof(gigyaGigyaDataCenter) == "undefined"){
url = gigyaURL + '/accounts.getAccountInfo?oauth_token=' + Keychain.get('gigyaCookieValue')
req = new Request(url)
apiResult = await req.loadString()
apiResult = JSON.parse(apiResult)
console.log("2.: " + apiResult.statusCode)
gigyaPersonID = apiResult.data.personId
gigyaGigyaDataCenter = apiResult.data.gigyaDataCenter
Keychain.set('gigyaPersonID', gigyaPersonID)
Keychain.set('gigyaGigyaDataCenter', gigyaGigyaDataCenter)
console.log('gigyaPersonID (new generated): ' + gigyaPersonID)
console.log('gigyaGigyaDataCenter (new generated): ' + gigyaGigyaDataCenter)
}
// 3. 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)
}
// 4. 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 not possible please try again later."
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)
}
}
// 5. 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 = "Request Successful."
sound = "piano_success"
} else {
pushBody = "There was an error seding the command no connection detected. Code:" + request.response.statusCode
sound = "piano_error"
}
pushMessage = new Notification()
pushMessage.identifier = "zoePostStatus"
if(endpoint == "hvac-start"){
pushMessage.title = "Preconditioning Requested"
}
if(endpoint == "charge-start"){
pushMessage.title = "STart Charging Requested"
}
//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
@edddeduck
Copy link
Author

edddeduck commented Nov 12, 2020

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 - My input was purely translating their work into English and adding in a quick and dirty conversion to Miles from KM.

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 2020), 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
  • NOW UPDATED: The script now displays miles. You can edit the sections above to revert to KM if you wish

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