Skip to content

Instantly share code, notes, and snippets.

@mountbatt
Last active April 18, 2024 09:18
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 2023-04-18
// 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
// 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
// uncomment what you need.
// use /* to disable */
// 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 == 0){
//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', 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)
}
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(api.Result.data)
}
// general function to POST status-values to our vehicle
async function postStatus(endpoint, jsondata, version, kamareonURL, account_id, VIN, gigyaJWTToken, kamareonAPI ){
url = kamareonURL + '/commerce/v1/accounts/' + account_id + '/kamereon/kca/car-adapter/v' + version + '/cars/' + VIN + '/actions/' + endpoint + '?country=DE'
request = new Request(url)
request.method = "POST"
request.body = jsondata
request.headers = { "x-gigya-id_token": gigyaJWTToken, "apikey": kamareonAPI, "Content-type": "application/vnd.api+json" }
apiResult = await request.loadString()
console.log(apiResult)
//debug:
// throw new Error(url)
let pushBody
let sound
if (request.response.statusCode == 200) {
pushBody = "Die Übermittlung des Befehls war erfolgreich."
sound = "piano_success"
} else {
pushBody = "Es ist ein Fehler beim Senden des Befehls aufgetreten. Keine Verbindung. Code:" + request.response.statusCode
sound = "piano_error"
}
pushMessage = new Notification()
pushMessage.identifier = "zoePostStatus"
if(endpoint == "hvac-start"){
pushMessage.title = "Kommando an Klimaanlage gesendet"
}
if(endpoint == "charge-start"){
pushMessage.title = "Kommando an Ladeanlage gesendet"
}
//pushMessage.title = "Befehl gesendet"
pushMessage.body = pushBody
pushMessage.sound = sound
//pushMessage.setTriggerDate(newDate);
pushMessage.schedule()
return apiResult
}
function time_convert(num)
{
var hours = Math.floor(num / 60);
var minutes = num % 60;
return hours + ":" + minutes;
}
// get images from local filestore or download them once
// this part is inspired by the dm-toilet-paper widget
// credits: https://gist.github.com/marco79cgn
async function getImage(image, imgUrl) {
let fm = FileManager.local()
let dir = fm.documentsDirectory()
let path = fm.joinPath(dir, image)
if (fm.fileExists(path)) {
return fm.readImage(path)
//fm.remove(path)
} else {
// download once
let imageUrl
switch (image) {
case 'my-renault-car-'+VIN+'.png':
imageUrl = imgUrl
break
default:
console.log(`Sorry, couldn't find ${image}.`);
}
if(imageUrl){
let iconImage = await loadImage(imageUrl)
fm.writeImage(path, iconImage)
return iconImage
}
}
}
// helper function to download an image from a given url
async function loadImage(imgUrl) {
const req = new Request(imgUrl)
return await req.loadImage()
}
// end of script
@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

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