Skip to content

Instantly share code, notes, and snippets.

@mountbatt
Last active April 19, 2024 06:07
Show Gist options
  • Star 61 You must be signed in to star a gist
  • Fork 10 You must be signed in to fork a gist
  • Save mountbatt/772e4512089802a2aa2622058dd1ded7 to your computer and use it in GitHub Desktop.
Save mountbatt/772e4512089802a2aa2622058dd1ded7 to your computer and use it in GitHub Desktop.
Scriptable iOS widget that displays the status of your Renault ZOE (or Megane, Dacia Spring) 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 2024-04-19
// latest changes:
// new kameron api key
// added language strings so you can translate it by yourself!
// add your my-renault account data:
// let myRenaultUser = "user" // email
// let myRenaultPass = "pass" // password
let myRenaultUser = "your_email" // email
let myRenaultPass = "your_password" // password
// set your ZOE Model (Phase 1 or 2) // bitte eingeben!
let ZOE_Phase = "2" // "1" or "2"
// set your battery size in kWh // bitte eingeben!
let ZOE_Battery = "52" // "52" or "41" or "22" or "21"
// 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"
// Language Strings
// GERMAN / DEUTSCH
let text_ladestand = "Ladestand"
let text_reichweite = "Reichweite"
let text_restenergie = "Restenergie"
let text_kilometerstand = "Kilometerstand"
let text_position = "Position"
let text_klimatisierung = "Klimatisierung"
let text_karte_oeffnen = "➤ Karte öffnen"
let text_starten = "➤ Starten"
let text_stoppen = "➤ Stoppen"
let text_wird_geladen = "⚡ Wird geladen …"
let text_gekoppelt = "🟢 Gekoppelt"
let text_entkoppelt = "⚫ Entkoppelt"
let text_laden_starten = "➤ Laden starten"
let text_akkutemp = "Akkutemp."
/*
// ENGLISH
let text_ladestand = "Charge level"
let text_reichweite = "Range"
let text_restenergie = "Remaining energy"
let text_kilometerstand = "Mileage"
let text_position = "Position"
let text_klimatisierung = "Climate control"
let text_karte_oeffnen = "➤ Open map"
let text_starten = "➤ Start"
let text_stoppen = "➤ Stop"
let text_wird_geladen = "⚡ Charging..."
let text_gekoppelt = "🟢 Connected"
let text_entkoppelt = "⚫ Disconnected"
let text_laden_starten = "➤ Start charging"
let text_akkutemp = "Battery temp."
*/
/*
// FRENCH:
let text_ladestand = "Niveau de charge"
let text_reichweite = "Autonomie"
let text_restenergie = "Énergie restante"
let text_kilometerstand = "Kilométrage"
let text_position = "Position"
let text_klimatisierung = "Climatisation"
let text_karte_oeffnen = "➤ Ouvrir la carte"
let text_starten = "➤ Démarrer"
let text_stoppen = "➤ Arrêter"
let text_wird_geladen = "⚡ En charge ..."
let text_gekoppelt = "🟢 Connecté"
let text_entkoppelt = "⚫ Déconnecté"
let text_laden_starten = "➤ Démarrer la charge"
let text_akkutemp = "Temp. de la batterie"
*/
// do not edit
let kamareonURL = "https://api-wired-prod-1-euw1.wrd-aws.com"
let kamareonAPI = "YjkKtHmGfaceeuExUDKGxrLZGGvtVS0J"
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 != 1) {
//plugIcon = await getImage("zoe-plug-off.png", "")
plugStateLabel = text_entkoppelt
} else {
//plugIcon = await getImage("zoe-plug-on.png", "")
plugStateLabel = text_gekoppelt
}
if (data.batteryStatus.attributes.chargingStatus == "1.0") {
plugStateLabel = text_wird_geladen
}
if (data.batteryStatus.attributes.plugStatus == 1 && data.batteryStatus.attributes.chargingStatus == "0") {
plugStateLabel = text_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(text_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(text_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(text_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(text_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(text_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(text_position)
LocationLabel.font = Font.mediumSystemFont(12)
const LocationVal = LocationStack.addText(text_karte_oeffnen)
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=ZOE&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(text_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(text_stoppen)
ac_url = `scriptable:///run?scriptName=${scriptName}&action=stop_ac`;
} else {
AcVal = AcStack.addText(text_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;
}
// 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', 1, 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)
}
//console.log(allResults)
// 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(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)
//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
@dehsgr
Copy link

dehsgr commented Feb 8, 2021

@db-EV you're right. Thanks for that hint! I'll change that in my code too.

@db-EV
Copy link

db-EV commented Feb 8, 2021

The ' sign seems to be wrong in the line 121. It's looks more like a french '. I hope you understand what I mean...

@dehsgr
Copy link

dehsgr commented Feb 8, 2021

@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.

@mountbatt
Copy link
Author

Dear readers/users please update to latest version 2021-02-09. Give thanks to @dehsgr

@coolio2004
Copy link

thanx for your hard work👍

@dehsgr
Copy link

dehsgr commented Feb 10, 2021

@mountbatt I just re-fixed charging link, if you would like to merge...

@mountbatt
Copy link
Author

@dehsgr thanks. Merged!

@dehsgr
Copy link

dehsgr commented Mar 17, 2021

@mountbatt there seem to be changes again to Renault API. Could you check for that?

@dehsgr
Copy link

dehsgr commented Mar 18, 2021

Error occurs in line 411. The only thing I see in other scripts they are using POST requests here instead of GET. Might this be the issue? 🤔

@dehsgr
Copy link

dehsgr commented Mar 18, 2021

Okay, just played a bit more.

carPicture = await apiResult.vehicleLinks[0].vehicleDetails.assets[0].renditions[0].url

is failing as Renault did some changes on their API again. The whole assets section seems to be missing in JSON data. So we‘ve to find another way for getting the car image...

@mountbatt
Copy link
Author

mountbatt commented Mar 18, 2021

@dehsgr Thanks for checking it. It looks like Renault changed the images on their server. (my car is now "looking" to the right) But i was able to get a new image without problems.

carPicture (new): https://3dv2.renault.com/ImageFromBookmark?configuration=X10%2FB10%2FEA3%2FDG%2FCAREG%2FVSTLAR%2FRET03%2FPROJAB%2FRALU17%2FDRAP13%2F3ATRPH%2FTEKPN%2FALEVA%2FVLCUI2%2FRETRCR%2FRETC%2FLVAREL%2FSGACHA%2FNA419%2FRNORM%2FTL09A%2FNBT022%2FDLIGM2%2FKITPOU%2FDANGMO%2FITPK4%2FVOLCHA%2FREACTI%2FAEBS07%2FPRAHL%2FRRCAM&databaseId=bfded4fa-7e22-4d4a-993d-8570c64d2eaa&bookmarkSet=CARPICKER&bookmark=EXT_34_RIGHT_FRONT&profile=HELIOS_OWNERSERVICES_LARGE

Before i added // on line 400 to force the downloading of the carPicture (and not getting it from keychain)

ImageFromBookmark

@dehsgr
Copy link

dehsgr commented Mar 18, 2021

@mountbatt but this is only a workaround, isn't it? I already commented out that line to get the image from keychain. But how we will get image url from API now?

@jblaster
Copy link

jblaster commented Apr 23, 2021

Thanks for this script, but I have some issue with it. I'm always having on error "Error on line 412:48 : TypeError : undefined is not an object (evaluating 'apiResult.vehicleLinks[0].vehicleDetails')
I'm from France, I've changed the Gigya Key in line 26 and the country to FR. Is there anything else to do?
Thanks for your help

@ghazlewood
Copy link

ghazlewood commented Jun 30, 2021

Thanks for the great script @mountbatt, I've been playing with this and have created a fork which contains a rudimentary translation setup so that both English and German can be chosen as the interface language. It should be simple to add a new translation for another language too.

@DJ-TMA
Copy link

DJ-TMA commented Aug 11, 2021

Hallo, seit ein paar Tagen aktualisiert scriptable das widget nicht mehr, weiß einer abhilfe?

@coolio2004
Copy link

coolio2004 commented Aug 11, 2021 via email

@Schoppeglas
Copy link

auch hier alles ok

@mountbatt
Copy link
Author

Hier läufts auch … @DJ-TMA ...

Copy link

ghost commented Sep 7, 2021

Thanks for this script, but I have some issue with it. I'm always having on error "Error on line 412:48 : TypeError : undefined is not an object (evaluating 'apiResult.vehicleLinks[0].vehicleDetails')
I'm from France, I've changed the Gigya Key in line 26 and the country to FR. Is there anything else to do?
Thanks for your help

@jblaster do you fix it ? I'm from France too and the script didn't work at all for this error :
Script Zoe FR error

Maybe it's because @mountbatt is from DE ?

@differentieel
Copy link

differentieel commented Oct 16, 2021

thnx for doing all this work - the script promises what i was looking for - except that error

First the CountryID

because of the Netherlands where i am - i changed 4 time's country=NL and wondered if it could be a variable as let CountryID = "NL" // check your countries ID to make the settings a bit more in line?

Now the error

i've carefully checked all lines but i still get in Scriptable:
Error on line 406:36: TypeError: undefined is not an object (evaluating 'apiResult.accounts[0]')
as already noticed the cause is probably in the lines above 406 - but which one?
BTW indefined is 19 times in the script
any suggestion is welcome
Undefined-error_0078-1280

@Stephan2020
Copy link

Hallo, ist es möglich eine Ladebegrenzung zu setzen? z.B nur laden bis 80%
Leider habe ich keine Ahnung vom programmieren.
Vielen Dank.
Gruß
Stephan

@mountbatt
Copy link
Author

Hallo, ist es möglich eine Ladebegrenzung zu setzen? z.B nur laden bis 80%
Leider habe ich keine Ahnung vom programmieren.
Vielen Dank.
Gruß
Stephan

Nein, da es sich nicht um eine App handelt. Das Widget wird nur ausgeführt, wenn man es aktiv als Nutzer öffnet. Und auch dann nur sporadisch. Da hat Apple seine Finger drauf. Daher eignet es sich nicht dafür um Benachrichtigungen oder gar lade Stopps auszuführen.

@dschuwa
Copy link

dschuwa commented Nov 14, 2021

Thanks for this script, but I have some issue with it. I'm always having on error "Error on line 412:48 : TypeError : undefined is not an object (evaluating 'apiResult.vehicleLinks[0].vehicleDetails')
I'm from France, I've changed the Gigya Key in line 26 and the country to FR. Is there anything else to do?
Thanks for your help

@jblaster do you fix it ? I'm from France too and the script didn't work at all for this error : Script Zoe FR error

Maybe it's because @mountbatt is from DE ?

Ich habe das gleiche Problem. Bei mir erscheint seit einiger Zeit die gleiche Fehlermeldung.
Das Script neu eingerichtet und sogar scriptable neu installiert brachte auch keine Lösung. Es scheint um das "CarPicture" zu gehen in Zeile 434.
Ob das mit einem iOS Update zusammenhängt, kann ich nicht sicher sagen. Derzeit habe ich iOS 15.1.

Würde mich über eine Lösung freuen.

Vielen Dank!

IMG_5163
.

@HenFei
Copy link

HenFei commented Dec 7, 2021

Hey, ist es möglich das man das Widget so anpasst, dass man mehrere Autos auf einmal sehen kann? Ich benötige eigentlich nur Den Autonamen/Kennzeigen und den Ladestatus. Es sollten dann 8 oder mehr Autos anzuzeigen sein.

@jackhiggins
Copy link

Hello. Wonder if anyone could help me figure out how to change the widget to display in miles in the following code?

// cockpitStatus // version: 2 // totalMileage = Num (in Kilometres!) let cockpitStatus = await getStatus('cockpit', 2, kamareonURL, account_id, VIN, gigyaJWTToken, kamareonAPI) allResults["cockpitStatus"] = cockpitStatus

@jumpjack
Copy link

km / 1.609 = miles

@jumpjack
Copy link

I am also working to a javascript source to access Renault API; it's not a widget but a standalone page, it's experimental and it needs a PHP proxy to access Renault/Gigya/Kamereon servers, so it is more useful for developers than for final users, to find out what I discovered about the API:
https://github.com/jumpjack/myRenaultLC

For example, in the repo you find info about how to retrieve charge history.

@plin2
Copy link

plin2 commented Mar 16, 2022

Hi looks like we need new API keys ... I'm getting access errors

@0eiki0
Copy link

0eiki0 commented Mar 16, 2022

EvneRuLYSX
Seit heute Mittag leider defekt :(

@dehsgr
Copy link

dehsgr commented Mar 16, 2022

@plin2 nope. No new API Keys are required. Seems that Renault changed https://accounts.eu1.gigya.com/accounts.getJWT from GET to POST call.

@db-EV
Copy link

db-EV commented Mar 16, 2022

No, I don't think so. My script ZoePHP is using POST for calling https://accounts.eu1.gigya.com/accounts.getJWT for a long time but now it stopped working. The script is getting stopped at the next step when calling https://api-wired-prod-1-euw1.wrd-aws.com. Therefore I think it's the Kamereon API key which has been changed.

@dehsgr
Copy link

dehsgr commented Mar 16, 2022

@db-EV seems you're right. New Key should be VAX7XYKGfa92yMvXculCkEFyfZbuM7Ss. But after changing I run into another error on line 380:26 --> Unexpected EOF.

@db-EV
Copy link

db-EV commented Mar 16, 2022

New Kamereon API key is working for my script. Thank you!
Maybe my PHP code will help you: https://github.com/db-EV/ZoePHP

@epenet
Copy link

epenet commented Mar 16, 2022

Thanks, new key is working in renault-api: hacf-fr/renault-api#552

@dehsgr
Copy link

dehsgr commented Mar 16, 2022

I'm online again too. @mountbatt should change line 27 to let kamareonAPI = "---entfernt---".

@plin2
Copy link

plin2 commented Mar 16, 2022

Key VAX7XYKGfa92yMvXculCkEFyfZbuM7Ss works in my solution as well

@mountbatt
Copy link
Author

Danke, @dehsgr und @plin2 - neue Version ist oben released.

@plin2
Copy link

plin2 commented Mar 16, 2022

In case somebody uses FHEM for home automation: https://forum.fhem.de/index.php/topic,116273.0.html

@jumpjack
Copy link

jumpjack commented Mar 17, 2022

Thanks, also my solution is working with new API key entfernt

@ionutze
Copy link

ionutze commented Mar 19, 2022

A2BABF1F-6D01-4DFD-B4F9-9D39071DBBAD

@plin2
Copy link

plin2 commented Mar 20, 2022

No error message on my side. Last login update was this morning at 3.

@matzZz
Copy link

matzZz commented Mar 24, 2022

Works also fine with Dacia Spring electric!

@mountbatt
Copy link
Author

Works also fine with Dacia Spring electric!

@matzZz nice! Please give us a screenshot

@matzZz
Copy link

matzZz commented Mar 25, 2022

Works also fine with Dacia Spring electric!

@matzZz nice! Please give us a screenshot

9A097381-2B4B-4286-892B-A75F9F8980E7

@RazvanGradinaru
Copy link

RazvanGradinaru commented Mar 27, 2022

Hello!
How can I solve this issue?
IMG_5865

@mountbatt
Copy link
Author

Try to add your VIN

@jumpjack
Copy link

jumpjack commented Apr 1, 2022

Maybe sniffing traffic of this Nissan app can help reversing engineering the SRP protocol?
https://play.google.com/store/apps/details?id=com.ideeo.nsnremote

@Tuedderbueddel
Copy link

Moin,

ich feiere das Widget seit einem Jahr. Seit dem letzten Update bekomme ich aber angehängte Fehlermeldung (SyntaxError: Invalid character '\u201e'). Ins Script habe ich an entsprechenden Stellen meinen Login (Mailadresse & PW) und die VIN geschrieben. Die Fahrzeugangaben stimmten alle von vornherein (Zoe 2, Akkugröße, etc.).

Hast du eine Idee, woran es noch liegen könnte?

Danke im Voraus!

Tuedderbueddel

IMG_2610

@mountbatt
Copy link
Author

mountbatt commented Apr 5, 2022

Moin,

ich feiere das Widget seit einem Jahr. Seit dem letzten Update bekomme ich aber angehängte Fehlermeldung (SyntaxError: Invalid character '\u201e'). Ins Script habe ich an entsprechenden Stellen meinen Login (Mailadresse & PW) und die VIN geschrieben. Die Fahrzeugangaben stimmten alle von vornherein (Zoe 2, Akkugröße, etc.).

Hast du eine Idee, woran es noch liegen könnte?

Danke im Voraus!

Tuedderbueddel

IMG_2610

@Tuedderbueddel Deutet auf falsche Anzahl Anführungszeichen hin. Schau mal in Zeile 7 oder etwas davor oder danach ob du dich da nicht vertan hast…

@Tuedderbueddel
Copy link

Tatsächlich, daran lag es! Bei Benutzer und PW war die deutsche Version der Anführungszeichen. Nach dem Ändern auf die englische funzt es nun. Vielen Dank und weiter so!

@druelie
Copy link

druelie commented Apr 30, 2022

Seit ein paar Tagen bekomme ich folgende Fehlermeldungen:

2022-04-30 17:52:15: Keychain cleared cause of action parameters
2022-04-30 17:52:15: gigyaPersonID (from keychain): undefined
2022-04-30 17:52:15: 1.: 200
2022-04-30 17:52:16: 3.: 200
2022-04-30 17:52:16: account_id (from keychain): undefined
2022-04-30 17:52:16: 4.: [object Object]
2022-04-30 17:52:16: Error on line 434:63: TypeError: undefined is not an object (evaluating 'apiResult.vehicleLinks[0].vehicleDetails.assets')

@druelie
Copy link

druelie commented Apr 30, 2022

Seit ein paar Tagen bekomme ich folgende Fehlermeldungen:

2022-04-30 17:52:15: Keychain cleared cause of action parameters 2022-04-30 17:52:15: gigyaPersonID (from keychain): undefined 2022-04-30 17:52:15: 1.: 200 2022-04-30 17:52:16: 3.: 200 2022-04-30 17:52:16: account_id (from keychain): undefined 2022-04-30 17:52:16: 4.: [object Object] 2022-04-30 17:52:16: Error on line 434:63: TypeError: undefined is not an object (evaluating 'apiResult.vehicleLinks[0].vehicleDetails.assets')

Das geht auch nur, wenn ich VIN hinzufüge.

@mountbatt
Copy link
Author

@dschuwa
Copy link

dschuwa commented May 2, 2022

Danke für das Update.
Leider funkt das Script bei mir weiterhin nicht. Ich hatte gehofft das sich das Problem mit dem Update löst. Bekomme weiterhin die Fehlermeldung: Error on line 451:56: TypeError: undefined is not an object (evaluating 'apiResult.vehicleLinks[carNumber].vehicleDetails').
FM

Kann das Script schon seit über 6 Monaten nicht mehr nutzen. Habe auch die VIN eintragen, Angaben zu Renault Login, ZOE und Akku passen alle.

Gibt es dafür eine Lösung oder eine Idee zur Lösung?

@mountbatt
Copy link
Author

mountbatt commented May 3, 2022

3.5.2022 – Kleines Update für alle, die mehrere Accounts bei gigya/Renault haben und keine Verbindung herstellen können. Probiert einfach in Zeile 30 die "accountNumber" des aktuellen Codes mal auf 1 statt 0 zu setzen.

https://gist.github.com/mountbatt/772e4512089802a2aa2622058dd1ded7?permalink_comment_id=3504088#gistcomment-3504088

@dschuwa

@dschuwa
Copy link

dschuwa commented May 3, 2022

3.5.2022 – Kleines Update für alle, die mehrere Accounts bei gigya/Renault haben und keine Verbindung herstellen können. Probiert einfach in Zeile 30 die "accountNumber" des aktuellen Codes mal auf 1 statt 0 zu setzen.

https://gist.github.com/mountbatt/772e4512089802a2aa2622058dd1ded7?permalink_comment_id=3504088#gistcomment-3504088

@dschuwa

Vielen Dank! Das ist die Lösung. Das Script funktioniert wieder.

@jumpjack
Copy link

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

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

@jumpjack
Copy link

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

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

@mountbatt
Copy link
Author

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

@Tuedderbueddel
Copy link

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

@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

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!

@codex-20
Copy link

Hi Mountbatt.

This is an awesome script. Many many thanks for taking the time to code it. I guess you’re a Zoe owner yourself!

My only suggestion would be to modify the code so that the language to be displayed is not hard coded in to the script but available to easily modify at the top with the rest of the user defined variables.

Thank you again in any case!

@RaMaHW
Copy link

RaMaHW commented Jan 16, 2023

Hi Mountbatt,
Thanks for the script, I have been using it successfully and frequently since almost the beginning.
FYI, I am currently getting some off values for the remaining charge. The attached result for a 52kWh ZOE. As far as I can tell from the log, the 13kWh is reported by the renault server.
8F6DD730-BE18-4A61-8023-6916F37EFA6D

@duracell
Copy link

Is there any idea when there will be an update about which complaints are there from renault, so that other people could avoid them?
And is it possible to work again on the script beside this complaints (e.g. if they are about the api key which doesn't need to be provided).

@mountbatt
Copy link
Author

Is there any idea when there will be an update about which complaints are there from renault, so that other people could avoid them?
And is it possible to work again on the script beside this complaints (e.g. if they are about the api key which doesn't need to be provided).

Dear @duracell , the latest version is still in the "reviews" section and it still works. But i will bring it back for all of you here.

@mountbatt
Copy link
Author

mountbatt commented Apr 16, 2023

Script is back! Enjoy … But there are no significant code changes since a few month, cause it still works, if Renaults servers are up and reachable!
new: Langauge strings - you can now translate it by yourself by changing layout strings - enjoy, @codex-20

@duracell
Copy link

Great :)
I knew that is available there, but some might not and I was not sure if it's a good idea to mention this if someone has a problem with the script. But good to hear that it's back and hopefully receive updates if renault changes anything.
Can you tell what renault wanted?

@octoplayer2
Copy link

octoplayer2 commented Apr 18, 2023

Any know if Renault have just changed the Kamereon API key again? ... I have been getting authentication errors ("access_denied, "Unauthorized") .

It started (or stopped) suddenly this morning around 9am. I can still log in on Renault App, so I dont think account itself has expired.

Regards
Kevin

UPDATE: Many thanks epenet for quick response :-)

@epenet
Copy link

epenet commented Apr 18, 2023

See hacf-fr/renault-api#848
New key: YjkKtHmGfaceeuExUDKGxrLZGGvtVS0J

@mountbatt
Copy link
Author

@epenet thanks a lot!!!!

@ionutze
Copy link

ionutze commented Mar 4, 2024

IMG_0333

@mountbatt
Copy link
Author

IMG_0333

Check your code again please.
maybe your password has some strange characters?
Or you made some errors with „“ characters!

@jumpjack
Copy link

jumpjack commented Mar 5, 2024

app updated on 26/2, probably new apikey, hence all 3rd party apps broken.

@db-EV
Copy link

db-EV commented Mar 5, 2024

No, ZoePHP and HA still working.

@ionutze
Copy link

ionutze commented Apr 15, 2024

does it still show your mileage?
IMG_0916

@ionutze
Copy link

ionutze commented Apr 18, 2024

It no longer displays km and kw.
Uploading IMG_0934.jpeg…

@epenet
Copy link

epenet commented Apr 18, 2024

For the cockpit information / mileage, see hacf-fr/renault-api#1145

@mountbatt
Copy link
Author

I released a small Update!
@ionutze @epenet
Renault switched the endpoint "cockpit" from version 2 to 1.

https://gist.github.com/mountbatt/772e4512089802a2aa2622058dd1ded7?permalink_comment_id=3504088#gistcomment-3504088

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