Car Stats Viewer | Simple Webhook API Anbindung

Hallo zusammen,

als stolzer PS2-Fahrer seit November und stiller Mitleser hier im Forum möchte ich mich an dieser Stelle kurz vorstellen. Mein Name ist Jonas und ich bin mit meinem PS2 SMLR MY24 in Magnesium im Raum Stuttgart unterwegs.

Ich bin großer Fan von @Ixam97’s fantastischer Car Stats Viewer App und finde – neben den ganzen Funktionen und Informationen innerhalb der App – vor allem die eingebaute API-Funktion extrem spannend.
Ich habe mich gefragt, wie ich die Daten, welche über die Webhook API gesendet werden, möglichst einfach und ohne großen Implementierungs- und Infrastruktur-Aufwand entgegen nehmen und speichern könnte.

Die banale Lösung: Google Sheets.
Ist kostenlos, in 5 min eingerichtet und läuft bei mir seit 2 Monaten stabil.

Das Ganze funktioniert mit Google Apps Script. Hier kann man ein kleines Script als öffentlich zugängliche Web App hinterlegen, welches ausgeführt wird, wenn Daten von CSV empfangen werden.
Die empfangenen Daten werden geparst und direkt Zeile für Zeile in das Google Sheet geschrieben.
Es werden automatisch vier Sheets für „Live Data“, „Driving Data“, „Charging Sessions“ und „Charging Points“, und jeweils Spalten für die einzelnen Werte wie „speed“, „stateOfCharge“, etc. angelegt. Zusätzlich werden zum einfacheren Debuggen der Daten zwei weitere Spalten erzeugt („dataPointReceived“ und „humanReadableDate“ bei „Live Data“).

Das Script ist aktuell noch sehr simpel und dient erstmal nur dazu, die Rohdaten von CSV zu speichern – als Datenbasis für spätere Auswertungen, Visualisierungen, etc.

Vielleicht interessant das den ein oder anderen?
So gehts …


  1. Mit Google Konto anmelden und neues Google Sheet erstellen.

  2. Im Menü: ExtensionsApps Script

  3. Code einfügen:

function doPost(e) {
  var sheet = SpreadsheetApp.getActiveSpreadsheet();
  var jsonData;
    
  // Parse the JSON content
  try {
      jsonData = JSON.parse(e.postData.contents);
      
  } catch (error) {
      return ContentService.createTextOutput("Error parsing JSON");
  }
  
  Logger.log(jsonData);
  // JSON keys
  var liveDataKeys = [
      'alt', 'ambientTemperature', 'apiVersion', 'appVersion', 'batteryLevel', 
      'chargePortConnected', 'ignitionState', 'lat', 'lon', 'power', 
      'selectedGear', 'speed', 'stateOfCharge', 'timestamp'
  ];
  var drivingPointKeys = [
      'alt', 'distance_delta', 'driving_point_epoch_time', 'energy_delta', 
      'lat', 'lon', 'point_marker_type', 'state_of_charge'
  ];
  var chargingSessionKeys = [
      'chargeTime', 'charged_energy', 'charged_soc', 'charging_session_id', 
      'end_epoch_time', 'lat', 'lon', 'outside_temp', 'start_epoch_time'
  ];
  var chargingPointKeys = [
      'charging_point_epoch_time', 'charging_session_id', 'energy_delta', 
      'point_marker_type', 'power', 'state_of_charge'
  ];

  // Process and append data for each data type
  processData('LiveData', [jsonData], liveDataKeys); // wrapped in array for uniformity
  processData('DrivingPoints', jsonData.drivingPoints, drivingPointKeys);
  processData('ChargingSessions', jsonData.chargingSessions, chargingSessionKeys);

  var allChargingPoints = jsonData.chargingSessions.flatMap(session => session.chargingPoints || []);
  processData('ChargingPoints', allChargingPoints, chargingPointKeys);

  return ContentService.createTextOutput(JSON.stringify({"result": "success"}))
      .setMimeType(ContentService.MimeType.JSON);
}

function processData(sheetName, dataArray, keys) {
  if (!dataArray) return; // Skip if data is not present

  // get the correct sheet, and create the sheet if not existing
  var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(sheetName) || SpreadsheetApp.getActiveSpreadsheet().insertSheet(sheetName);
  
  // if sheet is empty, add headers
  if (sheet.getLastRow() === 0) {
      var headerRow = keys.slice();
      if (sheetName === 'LiveData') {
        headerRow.push("humanReadable");
      }
      headerRow.push("dataPointReceived");
      sheet.appendRow(headerRow);
      sheet.setFrozenRows(1);
      sheet.getRange(1, 1, 1, keys.length).setFontWeight("bold");
  }

  // map incoming data to the headers, leave columns empty if data is missing
  var rows = dataArray.map(function(item) {
      var row = keys.map(function(key) {
           return (item[key] !== null && item[key] !== undefined) ? item[key] : ''; // leave empty if data is missing
      });
      
      // add human-readable date and time
      if (sheetName === 'LiveData' && item['timestamp']) {
          row.push(convertTimestampToDate(item['timestamp']));
      }
      // add timestamp for "dataPointReceived"
      row.push(new Date());
      return row;
  });

  // append data to sheet
  sheet.getRange(sheet.getLastRow() + 1, 1, rows.length, rows[0].length).setValues(rows);
}

// Helper function to convert timestamp to human-readable date
function convertTimestampToDate(timestamp) {
  if (!timestamp) return '';
  var date = new Date(timestamp);
  return Utilities.formatDate(date, Session.getScriptTimeZone(), "yyyy-MM-dd HH:mm:ss");
}


////////////////////////////////////////////////
// Testing

function test() {
  var testData = {
      postData: {
          contents: JSON.stringify({    
            "drivingPoints": [
                {
                    "alt": 2,                                   
                    "distance_delta": 19.714678,                
                    "driving_point_epoch_time": 1696070487231,  
                    "energy_delta": 45.35667,                   
                    "lat": 32.827896,                           
                    "lon": 7.4446335,                           
                    "point_marker_type": 2,                     
                    "state_of_charge": 1.0                      
                } 
            ],
            "chargingSessions": [
                {
                    "chargeTime": 51608,                  
                    "charged_energy": 932.0753670833333,  
                    "charged_soc": 2,                     
                    "chargingPoints": [                   
                        {
                            "charging_point_epoch_time": 1696071422457,
                            "charging_session_id": 1,
                            "energy_delta": 1,            
                            "point_marker_type": 1,       
                            "power": 1,                  
                            "state_of_charge": 1          
                        },{
                            "charging_point_epoch_time": 1696071422457,
                            "charging_session_id": 1,
                            "energy_delta": 2,            
                            "point_marker_type": 2,       
                            "power": 2,                  
                            "state_of_charge": 2          
                        }
                    ],
                    "charging_session_id": 1,          
                    "end_epoch_time": 1696071474074,
                    "lat": 32.827896,                  
                    "lon": 7.4446335,                  
                    "outside_temp": 2,                 
                    "start_epoch_time": 1696071422439
                }
            ],
            "alt":3,                        
            "ambientTemperature":0,         
            "apiVersion":"2.1",
            "appVersion":"0.25.2.0000",
            "batteryLevel":15000,           
            "chargePortConnected":false,
            "ignitionState":"On",
            "lat":32.827896,                
            "lon":7.4446335,                
            "power":-10,                     
            "selectedGear":"P",
            "speed":0,                      
            "stateOfCharge":1.0,            
            "timestamp":1696069418482
          })
      }
  };
  doPost(testData);
}
  1. Links bei „Services“ auf das + Icon (Add a service) klicken, aus der Liste Google Sheets API auswählen, und auf Add.

  2. Deploy → bei „Select type“ auf das Zahnrädchen-Icon klicken → Web App.
    „Execute as“: me
    „Who has access“: Anyone
    Dann auf Deploy.

  3. Authorize access → Google Account auswählen → Dann auf Advanced und auf Go to project (unsafe) klicken → Allow

  4. Web App URL kopieren und im PS2 in den CSV Settings hinterlegen. Fertig.


Das Script kann bei Bedarf getestet werden, indem man oben neben Run die Funktion test auswählt und dann auf Run klickt. Die deployte URL kann man mit einem beliebigen online tool (z.B. https://reqbin.com) testen.

Ich freue mich über Feedback, Ideen und Verbesserungsvorschläge.

25 „Gefällt mir“

Sehr interessanter Ansatz! Ich wusste gar nicht, dass sowas bei Google geht. Ein mal Aufgesetzt hat man so ein echt komfortables Export-Feature gebastelt. Finde ich gut!

Der CSV hat ja auch die Möglichkeit, die ganze lokale Datenbank in einem Rutsch hochzuladen. Hast du das auch schon ausprobiert? Ich glaube, da werden momentan noch die ChargingPoints ausgelassen, und nur die Sessions hochgeladen. Da müsste ich aber noch mal nachsehen und ggf. nachbessern. Dann wäre das eine echt nette Möglichkeit, das lange gewünschte Export-Feature umzusetzen. :thinking:

4 „Gefällt mir“

@jonas
Hi Jonas,
Vielen Dank, mit deiner Anleitung habe selbst ich, als Computer-Legastheniker, das hin bekommen.

Womit ich allerdings nicht klar komme sind die gespeicherten Speed-Werte. Die bewegen sich zwischen 0 und 40. Ich bin bestimmt 140 km/h gefahren - aber irgendwie passt das nicht. Muss man die umrechnen? Wenn Ja, wie? Oder @Ixam97 Maxi weißt du wie?

1 „Gefällt mir“

Die Geschwindigkeit wird in m/s angegeben: 40 m/s * 3,6 → 144 km/h.

Danke, da wäre ich nie drauf gekommen.
Wäre es möglich die Einheiten in die Header-Zeile zu schreiben? Bei power und batteryLevel bin schon wieder am raten…

Das ist alles in der API-Dokumentation beschrieben:

1 „Gefällt mir“

Super – danke euch, das freut mich!


Gute Idee! Habe ich tatsächlich noch nicht ausprobiert.
Werde das die Tage mal machen und berichten …

Servus zusammen,

bin dabei das ganze auch mal zu Testen, da mich Metriken und Meta Daten auch beruflich sehr interessieren.

Anbei ein paar Screenshots, evtl hilft es dem ein oder anderen:

  1. Mit Google Konto anmelden und neues Google Sheet erstellen.

  2. Im Menü: ExtensionsApps Script

  3. Code einfügen:

  4. Deploy → bei „Select type“ auf das Zahnrädchen-Icon klicken → Web App.
    „Execute as“: me
    „Who has access“: Anyone
    Dann auf Deploy.

  1. Authorize access → Google Account auswählen → Dann auf Advanced und auf Go to project (unsafe) klicken → Allow

Jetzt bleibt mir nur noch die Frage, wo ich im Polestar 2 die CSV eintragen kann, evtl. kann mir hier einer von Euch aushelfen …

Danke im voraus.

2 „Gefällt mir“

in der carstats viewer app. Mehr dazu ist hier in der doku:

bzw. wenn du die noch nicht installiert hast dann findest du hier mehr infos:

Danke fürs teilen, in der Tat habe ich die App noch nicht installiert.
Warte aktuell auf Teilnahme in einem Test Track.
Also erstmal abwarten und Tee trinken :wink:

1 „Gefällt mir“

Habe heute mal auf den Upload Button gedrückt. Charging Sessions kamen vollständig an. Driving Points nur teilweise, der Rest gar nicht. :sob:

Eine Vermutung wäre, dass Google irgendwann dicht macht wegen zu vielen schnellen Aufrufen des Endpunkts, oder zu vielen Daten … ?

Wie genau werden denn die Daten beim Upload gesendet (alles auf einmal in einem JSON Paket oder „häppchenweise“) und in welchem Format (gleich wie sonst auch)?

Das Format ist exakt identisch wie bei den „Echtzeit“-Daten. Allerdings werden die Datenpunkte gruppiert. Es werden also immer mehrere gemeinsam verschickt, damit ein einzelner Request nicht zu groß wird. Das ganze ist dann einfach ein JSON-Array. Übertragen werden auch nur die „Drive Point“-Daten, die sowieso regulär gespeichert werden. Die Daten, die alle paar Sekunden gesendet werden können, werden nicht lokal gespeichert. Das sieht dann von der Struktur her in etwa so aus:

{
    "alt":0,
    "ambientTemperature":0,  
//...
    "drivingPoints": [
        {...},
//...
        {...}
    ],
    "chargingSessions": [...]
}

Kann es sein, dass das Skript die Arrays nicht vernünftig auswertet und jeweils nur der erste Datenpunkt übernommen wird?

Ansonsten wäre es durchaus denkbar, dass Google da irgendwann dicht macht :confused:

1 „Gefällt mir“

Okay, das heißt …

  • DrivingPoints (alle 100m): wird gesendet
  • ChargingSessions: wird gesendet
  • ChargingPoints: wird nicht gesendet
  • LiveData (alle 5s): wird nicht gesendet

Korrekt?
Dann scheint es prinzipiell ja zu funktionieren. Ich teste nochmal.

Gibt es eine einfache Möglichkeit wie ich die URL in den CSV bekomme?

Ich muss die Web-App URL angeben, richtig?
Also Benutzername und Passwort dann entsprechend meine Googledaten?

Genau – die Web App URL im CSV eintragen. Ist viel zu lang um das händisch zu machen…

Ich hab mir die URL am Rechner als Notiz in Google Keep notiert, dann im Auto mit Vivaldi Browser aufgerufen, kopiert, und schließlich im CSV eingefügt.

Läuft, danke fürs howto

1 „Gefällt mir“

Hi @jonas

Drei schnelle Fragen dazu:

  1. Wie lange dauert ein initialer Upload?
  2. Kann man das während der Fahrt machen?
  3. Legt er dabei eine Tabelle an und ergänzt die später um weitere Einträge?
1 „Gefällt mir“
  1. So lange du den Gesamt-Daten-Export nicht startest gibt es keinen initialen Upload. Die Datenerfassung im Google Sheet startet mit den ersten gesendeten „Live“-Daten.
    Den Daten-Export selbst habe ich in der Vergangenheit nur einmal getestet, mit mittlerem Erfolg (siehe weiter oben). Du kannst es ja einfach mal probieren …

  2. Ob der Daten-Export während der Fahrt funktioniert, weiß ich nicht. @Ixam97 ?

  3. Genau, die ersten Daten die beim Google Sheet ankommen legen jeweils die Tabellen Sheets an, alle weiteren Daten werden als Zeilen angefügt.

15min gewartet.
CSV sagt, dass Upload erfolgreich ist.
Tabelle ist allerdings leer.

Das hängt davon ab, wie viele Daten bereits in CSV gespeichert sind. Pauschal lässt sich das nicht sagen.

Ich bin selber noch nicht dazu gekommen, die Sheets-Lösung mal auszuprobieren. Wenn ich die Zeit finde werde ich das mal machen und schauen, wo es klemmt.