myAudi Klasse für Fahrzeugdaten

Hallo zusammen,
ich wollte hier mal meine Klasse für den Zugriff auf myAudi zur Verfügung stellen, die bei mir seit einiger Zeit läuft.
Ich hatte Probleme mit der Anbindung über ioBroker, da mein PHEV Audi A3 TFSIe nicht mehr voll unterstützt wurde (Bedienung der Standklimatisierung) und habe dann ioBroker komplett abgelöst und in IP-Symcon integriert. :loveips:

Vieles in der Klasse ist von anderen zusammengetragen und übernommen, vor allem von https://github.com/thomasesmith/vw-car-net-api/blob/master/README.md und von Test Adapter VW Connect für VW, ID, Audi, Seat, Skoda

Da ich einige der Abfragen nur mit GuzzleHttp zum Laufen bekommen habe, musste ich mir einen vendor-Ordner basteln und unter C:\ProgramData\Symcon\scripts ablegen. Dieser kann unter vendor.zip von OneDrive geladen werden.

Mein A3 PHEV hat hier wohl eine Besonderheit, dass sowohl die alte als auch die neue API von Audi abgefragt werden. Die Klimaanlage und das Fahrtenbuch sind noch in der alten API.

Ich werde nicht in der Lage sein, Support für das hier zu leisten, aber vielleicht hat ja jemand Lust mehr daraus zu machen.

Die Kurzanleitung wäre dann:

  1. Den vendor-Ordner aus der Zip-Datei zusammen mit der Skript Audi.class.php nach C:\ProgramData\Symcon\scripts extrahieren und das Skript in Symcon importieren.

  2. In der Audi.class.php die Zugangsdaten, FIN und die ID der Startkategorie für die Objekte eintragen.

  3. Skript erstellen und darin die Klasse includen und die gewünschte Abfrage ausführen, also z.B.:

<?php
include ("AudiClass.ips.php");
$audi->getCarData();

Da andere Autos andere Funktionen und Endpunkte haben, wird bei Euch ggfls. einiges anders sein.
Das Konzept sollte aber auch für andere Fahrzeuge mit App-Anbindung aus dem VW-Konzern funktionieren, die dafür nötigen Infos müsste man sich dann über die oben verlinkten Seiten selbst zusammen suchen. Außerdem benutze ich für einige Funktionen eigene Daten, deren IDs Ihr vermutlich auch anpassen müsst.

Wenn es gut läuft, solltet ihr unter der rootID dann ungefähr so etwas finden:

Und hier nochmal die Klasse selbst:

<?php
require 'vendor/autoload.php';
$audi = new audi();


//print_r($audi->getCarData());

//print_r($audi->getStatus());
//$audi->getStatus();
//print_r($audi->getParkingposition());
//print_r($audi->getTripdata('shortTerm'));
//print_r($audi->getTripdata('longTerm'));
//print_r($audi->getClimater());

//print_r($audi->setClimater('startClimatisation'));
//print_r($audi->setClimater('stopClimatisation'));
//print_r($audi->setCharger('timer')); // Stopt das Laden
//print_r($audi->setCharger('manual')); // Startet das Laden

//print_r($audi->vehiclewakeup());
//print_r($audi->getAllAuthenticationTokens());

class audi{


//################################################################################ Einstellungen ####################################################################################


    private $emailAddress = "useraccount@audi.de";     //myAudi-Login Mailadresse
    private $password = "Passwort";                    //myAudi-Login Kennwort
    private $securityPIN = "1234";                      //PIN für sicherheitsrelevante Funktionen, wird noch nicht verwendet.
    private $vin = "WAUZZZ*******";                 //FIN des Audi
    private $rootId = 23309;                       //Startkategorie, unterhalb der alle Objekte erstellt werden
    private $tripDataMax = 2000;                        //Maximale Anzahl der letzten Einträge aus dem Fahrtenbuch, nur für Anzeige, geladen werden alle
    private $responseCache = true;
    private $debug = false;


//################################################################## Ab hier musst Du wissen, was Du tust... #########################################################################


    private $access_token;
    private $refresh_token;
    private $vwaccess_token;
    private $vwrefresh_token;
    protected $curl;

    private $state; 
    private $codeChallenge; 
    private $codeVerifier;
    private $csrf; 
    private $relayState; 
    private $hmac; 
    private $nextFormAction; 
    private $code; 
    private $cookies;
    private $saveCallback;
    private $authType; // 'audi' oder 'audietron'
    
    const API_HOST_AUDIETRON = 'https://emea.bff.cariad.digital';
    const API_HOST_AUDI = 'https://mal-3a.prd.eu.dp.vwg-connect.com';
    const AUTH_HOST = 'https://identity.vwgroup.io';
    const AUTH_USER_AGENT_SPOOF = "Android/4.18.0 (Build 800239240.root project 'onetouch-android'.ext.buildTime) Android/11";
    const APP_USER_AGENT_SPOOF = 'Car-Net/60 CFNetwork/1121.2.2 Darwin/19.3.0';
    const APP_CLIENT_ID = 'f4d0934f-32bf-4ce4-b3c4-699a7049ad26@apps_vw-dilab_com';



	function __construct(){
		$this->curl = curl_init();
        $this->client = new GuzzleHttp\Client();
        $this->clientCookieJar = new GuzzleHttp\Cookie\CookieJar();        
        $this->getTokens();
	}
	

	function __destruct(){
    	curl_close($this->curl);
    }



//####################################################################### Fahrzeug Funktionen ###############################################################################


    public function getCarData(){
        $data = (object)[];
        $data->getStatus = $this->getStatus();
        $data->getParkingposition = $this->getParkingposition();
        $data->getClimater = $this->getClimater();
        $data->getTimersandProfiles = $this->getTimersandProfiles();
        //$data->getTripdata = $this->getTripdata(); //Dauert manchmal sehr lange
        return $data;
    }


	public function getStatus(){
        curl_setopt($this->curl, CURLOPT_URL, self::API_HOST_AUDIETRON . '/vehicle/v1/vehicles/' . $this->vin . '/selectivestatus?jobs=charging%2CchargingTimers%2CchargingProfiles%2CfuelStatus%2Cmeasurements%2CoilLevel%2CvehicleHealthInspection%2Caccess%2CvehicleLights');
        curl_setopt($this->curl, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($this->curl, CURLOPT_HTTPHEADER, array(
            'accept: application/json',
            'authorization: Bearer '.$this->access_token,
        ));
        
        $getStatus = json_decode(curl_exec($this->curl));
        if (curl_errno($this->curl)) {
            $error_msg = curl_error($this->curl);
            return $error_msg;
        }

        if($this->responseCache){
            $getStatusCacheId = $this->CreateVariableByIdent($_IPS['SELF'], '_getStatusCache', 'AC_getStatusCache', 3, $profile = "");
            SetValueString($getStatusCacheId, json_encode($getStatus));
        }

        foreach($getStatus as $object=>$item){
            $InstanzID = @IPS_GetInstanceIDByName($object, $this->rootId);
            if ($InstanzID === false){
                $InstanzID = IPS_CreateInstance("{485D0419-BE97-4548-AA9C-C083EB82E61E}");
                IPS_SetName($InstanzID, $object);
                IPS_SetParent($InstanzID, $this->rootId);
                IPS_ApplyChanges($InstanzID);
            }

            foreach($item as $subobject=>$subitem){
                $subInstanzID = @IPS_GetInstanceIDByName($subobject, $InstanzID);
                if ($subInstanzID === false){
                    $subInstanzID = IPS_CreateInstance("{485D0419-BE97-4548-AA9C-C083EB82E61E}");
                    IPS_SetName($subInstanzID, $subobject);
                    IPS_SetParent($subInstanzID, $InstanzID);
                    IPS_ApplyChanges($subInstanzID);
                }
                if(isset($subitem->value)){
                    foreach($subitem->value as $key=>$value){
                        if(!is_array($value) && !is_object($value)){
                            if(gettype($value) == 'boolean')$type = 0;
                            if(gettype($value) == 'integer')$type = 1;
                            if(gettype($value) == 'double')$type = 2;
                            if(gettype($value) == 'string')$type = 3;
                            $vid = $this->CreateVariableByIdent($subInstanzID, $key, $key, $type, $profile = "");
                            if(GetValue($vid) != $value)SetValue($vid,$value);
                        }
                        elseif(is_object($value)){
                            $name = isset($value->id)?$value->id:$value->type;
                            $capabilitiesStatusID = @IPS_GetInstanceIDByName($name, $subInstanzID);
                            if ($capabilitiesStatusID === false){
                                $capabilitiesStatusID = IPS_CreateInstance("{485D0419-BE97-4548-AA9C-C083EB82E61E}");
                                IPS_SetName($capabilitiesStatusID, $name);
                                IPS_SetParent($capabilitiesStatusID, $subInstanzID);
                                IPS_ApplyChanges($capabilitiesStatusID);
                            }
                            foreach($value as $variableName => $variableValue){
                                if($variableName !== 'id' && $variableName !== 'type' && !is_array($variableValue)){
                                    if(gettype($variableValue) == 'boolean')$type = 0;
                                    if(gettype($variableValue) == 'integer')$type = 1;
                                    if(gettype($variableValue) == 'double')$type = 2;
                                    if(gettype($variableValue) == 'string')$type = 3;
                                    $vid = $this->CreateVariableByIdent($capabilitiesStatusID, $variableName, $variableName, $type, $profile = "");
                                    if(GetValue($vid) != $variableValue)SetValue($vid,$variableValue);
                                }
                                elseif(is_array($variableValue)){
                                }
                            }
                        }
                        elseif(is_array($value)){
                            if(IPS_GetName($subInstanzID)=='accessStatus' || IPS_GetName($subInstanzID)=='lightsStatus'){
                                $capabilitiesStatusID = @IPS_GetInstanceIDByName($key, $subInstanzID);
                                if ($capabilitiesStatusID === false){
                                    $capabilitiesStatusID = IPS_CreateInstance("{485D0419-BE97-4548-AA9C-C083EB82E61E}");
                                    IPS_SetName($capabilitiesStatusID, $key);
                                    IPS_SetParent($capabilitiesStatusID, $subInstanzID);
                                    IPS_ApplyChanges($capabilitiesStatusID);
                                }
                                foreach($value as $subObject){
                                    $vid = $this->CreateVariableByIdent($capabilitiesStatusID, $subObject->name, $subObject->name, 3, $profile = "");
                                    if(GetValue($vid) != json_encode($subObject->status))SetValue($vid,json_encode($subObject->status));
                                }
                            }
                        }
                    }
                }
            }
        }
		return $getStatus;
	}


	public function getParkingposition(){
        curl_setopt($this->curl, CURLOPT_URL, self::API_HOST_AUDIETRON . '/vehicle/v1/vehicles/' . $this->vin . '/parkingposition');
        curl_setopt($this->curl, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($this->curl, CURLOPT_HTTPHEADER, array(
            'accept: application/json',
            'authorization: Bearer '.$this->access_token,
        ));
        $getParkingposition = json_decode(curl_exec($this->curl));
        if (curl_errno($this->curl)) {
            $error_msg = curl_error($this->curl);
            return $error_msg;
        }

        if($this->responseCache){
            $getParkingpositionCacheId = $this->CreateVariableByIdent($_IPS['SELF'], '_getParkingpositionCache', 'AC_getParkingpositionCache', 3, $profile = "");
            SetValueString($getParkingpositionCacheId, json_encode($getParkingposition));
        }

        $InstanzID = @IPS_GetInstanceIDByName('parkingposition', $this->rootId);
        if ($InstanzID === false){
            $InstanzID = IPS_CreateInstance("{485D0419-BE97-4548-AA9C-C083EB82E61E}");
            IPS_SetName($InstanzID, 'parkingposition');
            IPS_SetParent($InstanzID, $this->rootId);
            IPS_ApplyChanges($InstanzID);
        }

        $lat = $this->CreateVariableByIdent($InstanzID, 'lat', 'lat', 2, $profile = "");
        if(GetValue($lat) != $getParkingposition->data->lat)SetValue($lat,$getParkingposition->data->lat);
        $lon = $this->CreateVariableByIdent($InstanzID, 'lon', 'lon', 2, $profile = "");
        if(GetValue($lon) != $getParkingposition->data->lon)SetValue($lon,$getParkingposition->data->lon);
		return $getParkingposition;
	}

    
	public function getClimater(){
        curl_setopt($this->curl, CURLOPT_URL, self::API_HOST_AUDI . '/api/bs/climatisation/v1/vehicles/' . $this->vin . '/climater');
        curl_setopt($this->curl, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($this->curl, CURLOPT_HTTPHEADER, array(
            'accept: application/json',
            'authorization: Bearer '.$this->vwaccess_token,
			'accept-charset: utf-8',
			'X-App-Version: 4.18.0',
			'X-App-Name: myAudi',
			'X-Client-Id: a09b50fe-27f9-410b-9a3e-cb7e5b7e45eb',
			"user-agent: Android/4.18.0 (Build 800239240.root project 'onetouch-android'.ext.buildTime) Android/11",
			'Host: mal-3a.prd.eu.dp.vwg-connect.com'
        ));
        $getClimater = json_decode(curl_exec($this->curl));
        if($this->debug)print_r($getClimater);        
        if (curl_errno($this->curl)) {
            $error_msg = curl_error($this->curl);
            return $error_msg;
        }

        if($this->responseCache){
            $getClimaterCacheId = $this->CreateVariableByIdent($_IPS['SELF'], '_getClimaterCache', 'AC_getClimaterCache', 3, $profile = "");
            SetValueString($getClimaterCacheId, json_encode($getClimater));
        }

        $InstanzID = @IPS_GetInstanceIDByName('climater', $this->rootId);
        if ($InstanzID === false){
            $InstanzID = IPS_CreateInstance("{485D0419-BE97-4548-AA9C-C083EB82E61E}");
            IPS_SetName($InstanzID, 'climater');
            IPS_SetParent($InstanzID, $this->rootId);
            IPS_ApplyChanges($InstanzID);
        }

        $settingsID = @IPS_GetInstanceIDByName('settings', $InstanzID);
        if ($settingsID === false){
            $settingsID = IPS_CreateInstance("{485D0419-BE97-4548-AA9C-C083EB82E61E}");
            IPS_SetName($settingsID, 'settings');
            IPS_SetParent($settingsID, $InstanzID);
            IPS_ApplyChanges($settingsID);
        }

        $statusID = @IPS_GetInstanceIDByName('status', $InstanzID);
        if ($statusID === false){
            $statusID = IPS_CreateInstance("{485D0419-BE97-4548-AA9C-C083EB82E61E}");
            IPS_SetName($statusID, 'status');
            IPS_SetParent($statusID, $InstanzID);
            IPS_ApplyChanges($statusID);
        }

        foreach($getClimater->climater->settings as $setting => $value){
            if(gettype($value->content) == 'boolean')$type = 0;
            if(gettype($value->content) == 'integer')$type = 1;
            if(gettype($value->content) == 'double')$type = 2;
            if(gettype($value->content) == 'string')$type = 3;
            $vid = $this->CreateVariableByIdent($settingsID, $setting, $setting, $type, $profile = "");
            if(GetValue($vid) != $value->content)SetValue($vid,$value->content);
        }

        foreach($getClimater->climater->status->climatisationStatusData as $status => $value){
            if(gettype($value->content) == 'boolean')$type = 0;
            if(gettype($value->content) == 'integer')$type = 1;
            if(gettype($value->content) == 'double')$type = 2;
            if(gettype($value->content) == 'string')$type = 3;
            $vid = $this->CreateVariableByIdent($statusID, $status, $status, $type, $profile = "");
            if(GetValue($vid) != $value->content)SetValue($vid,$value->content);
        }

        foreach($getClimater->climater->status->vehicleParkingClockStatusData as $status => $value){
            if(gettype($value->content) == 'boolean')$type = 0;
            if(gettype($value->content) == 'integer')$type = 1;
            if(gettype($value->content) == 'double')$type = 2;
            if(gettype($value->content) == 'string')$type = 3;
            $vid = $this->CreateVariableByIdent($statusID, $status, $status, $type, $profile = "");
            if(GetValue($vid) != $value->content)SetValue($vid,$value->content);
        }
		return $getClimater;
	}


    public function setClimater($action){
        curl_setopt($this->curl, CURLOPT_URL, self::API_HOST_AUDI . '/api/bs/climatisation/v1/vehicles/' . $this->vin . '/climater/actions');
        curl_setopt($this->curl, CURLOPT_CUSTOMREQUEST, "POST");
        curl_setopt($this->curl, CURLOPT_POSTFIELDS, '{"action":{"type":"'.$action.'","settings":{"heaterSource":"electric"}}}');
        curl_setopt($this->curl, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($this->curl, CURLOPT_HTTPHEADER, array(
            'accept: application/json',
            'authorization: Bearer '.$this->vwaccess_token,
			'accept-charset: utf-8',
			'X-App-Version: 4.18.0',
			'X-App-Name: myAudi',
			'X-Client-Id: a09b50fe-27f9-410b-9a3e-cb7e5b7e45eb',
			"user-agent: Android/4.18.0 (Build 800239240.root project 'onetouch-android'.ext.buildTime) Android/11",
			'Host: mal-3a.prd.eu.dp.vwg-connect.com',
            'Content-Type: application/json; charset=utf-8'
        ));
        $response = curl_exec($this->curl);
        if (curl_errno($this->curl)) {
            $error_msg = curl_error($this->curl);
            return $error_msg;
        }
		return json_decode($response);
    }


    public function getTripdata($tripType = 'shortTerm'){
        curl_setopt($this->curl, CURLOPT_URL, self::API_HOST_AUDI . '/api/bs/tripstatistics/v1/vehicles/' . $this->vin . '/tripdata/'.$tripType.'?type=list');
        curl_setopt($this->curl, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($this->curl, CURLOPT_HTTPHEADER, array(
            'accept: application/json',
            'authorization: Bearer '.$this->vwaccess_token,
			'accept-charset: utf-8',
			'X-App-Version: 4.18.0',
			'X-App-Name: myAudi',
			'X-Client-Id: a09b50fe-27f9-410b-9a3e-cb7e5b7e45eb',
			"user-agent: Android/4.18.0 (Build 800239240.root project 'onetouch-android'.ext.buildTime) Android/11",
			'Host: mal-3a.prd.eu.dp.vwg-connect.com'
        ));
        $response = curl_exec($this->curl);
        if (curl_errno($this->curl)) {
            $error_msg = curl_error($this->curl);
            return $error_msg;
        }
        if($this->responseCache){
            $getTripdataCacheId = $this->CreateVariableByIdent($_IPS['SELF'], '_getTripdataCache', 'AC_getTripdataCache', 3, $profile = "");
            SetValueString($getTripdataCacheId, json_encode($response));        
        }

        if($tripType == 'longTerm'){
            $response = json_decode($response);
            unset($response->tripDataList->tripData[0]);
            $response->tripDataList->tripData = array_values($response->tripDataList->tripData);

            $InstanzID = @IPS_GetInstanceIDByName('tripData', $this->rootId);
            if ($InstanzID === false){
                $InstanzID = IPS_CreateInstance("{485D0419-BE97-4548-AA9C-C083EB82E61E}");
                IPS_SetName($InstanzID, 'tripData');
                IPS_SetParent($InstanzID, $this->rootId);
                IPS_ApplyChanges($InstanzID);
            }

            $longTermID = @IPS_GetInstanceIDByName('longTerm', $InstanzID);
            if ($longTermID === false){
                $longTermID = IPS_CreateInstance("{485D0419-BE97-4548-AA9C-C083EB82E61E}");
                IPS_SetName($longTermID, 'longTerm');
                IPS_SetParent($longTermID, $InstanzID);
                IPS_ApplyChanges($longTermID);
            }

            foreach($response->tripDataList->tripData[0] as $status => $value){
                if(gettype($value) == 'boolean')$type = 0;
                if(gettype($value) == 'integer')$type = 1;
                if(gettype($value) == 'double')$type = 2;
                if(gettype($value) == 'string')$type = 3;
                $vid = $this->CreateVariableByIdent($longTermID, $status, $status, $type, $profile = "");
                if(GetValue($vid) != $value)SetValue($vid,$value);
            }
            return $response;
        }
        else{
            $tripdatashortTerm = json_decode($response)->tripDataList->tripData;
            //Sortieren nach timestamp
            usort($tripdatashortTerm, function($a, $b) {
                return $b->timestamp <=> $a->timestamp;
            });
            //Abschneiden nach $tripDataMax Einträgen
            $tripdatashortTerm = array_slice($tripdatashortTerm,0,$this->tripDataMax);

            $InstanzID = @IPS_GetInstanceIDByName('tripData', $this->rootId);
            if ($InstanzID === false){
                $InstanzID = IPS_CreateInstance("{485D0419-BE97-4548-AA9C-C083EB82E61E}");
                IPS_SetName($InstanzID, 'tripData');
                IPS_SetParent($InstanzID, $this->rootId);
                IPS_ApplyChanges($InstanzID);
            }

            $shortTermID = @IPS_GetInstanceIDByName('shortTerm', $InstanzID);
            if ($shortTermID === false){
                $shortTermID = IPS_CreateInstance("{485D0419-BE97-4548-AA9C-C083EB82E61E}");
                IPS_SetName($shortTermID, 'shortTerm');
                IPS_SetParent($shortTermID, $InstanzID);
                IPS_ApplyChanges($shortTermID);
            }

            foreach($tripdatashortTerm as $object){
                $name = preg_replace('/[^a-z\d ]/i', '',$object->timestamp);
                $tripID = @IPS_GetInstanceIDByName($name, $shortTermID);
                if ($tripID === false){
                    $tripID = IPS_CreateInstance("{485D0419-BE97-4548-AA9C-C083EB82E61E}");
                    IPS_SetName($tripID, $name);
                    IPS_SetParent($tripID, $shortTermID);
                    IPS_ApplyChanges($tripID);
                }

                foreach($object as $key => $value){
                    if(gettype($value) == 'boolean')$type = 0;
                    if(gettype($value) == 'integer')$type = 1;
                    if(gettype($value) == 'double')$type = 2;
                    if(gettype($value) == 'string')$type = 3;
                    $vid = $this->CreateVariableByIdent($tripID, $key, $key, $type, $profile = "");
                    if(GetValue($vid) != $value)SetValue($vid,$value);
                }
            }
            return $tripdatashortTerm;
        }
	}


    public function getTripDataHTML(){
        $tripdatalongTerm = $this->getTripdata('longTerm')->tripDataList->tripData[0];
        $tripdatashortTerm = $this->getTripdata('shortTerm');

        //print_r($tripdatashortTerm);

        $shortTerm_mileage = 0;
        $shortTerm_traveltime = 0;
        $shortTerm_averageSpeed = 0;
        $shortTerm_overallElectricEngineConsumption = 0;
        $shortTerm_overallFuelConsumption = 0;
        $shortTerm_averageElectricEngineConsumption = 0;
        $shortTerm_averageFuelConsumption = 0;
        $tripdatashortTermHTML = '';

        $verbrauchHTML='';


        $Strompreis = GetValue(35427);
        foreach($tripdatashortTerm as $trip){
            $verbrauchHTML .= '<tr align="center"><td>' . date("d.m.Y H:i:s",strtotime($trip->timestamp)) . "</td>";
            $verbrauchHTML .= "<td>" . $trip->mileage . " km</td>";
            $verbrauchHTML .= "<td>" . intdiv($trip->traveltime, 60).':'. (strlen(($trip->traveltime % 60)) == 1 ? '0':'') . ($trip->traveltime % 60) . " h</td>";
            $verbrauchHTML .= "<td>" . $trip->averageSpeed . " km/h</td>";
            $verbrauchHTML .= "<td>" . (isset($trip->averageFuelConsumption) ?  $trip->averageFuelConsumption / 10 : 0) . " l</td>";
            $verbrauchHTML .= "<td>" . $trip->averageElectricEngineConsumption / 10 . " kWh</td>";
            $verbrauchHTML .= "<td>" . (isset($trip->averageFuelConsumption) ?  $trip->averageFuelConsumption / 1000 * $trip->mileage : 0) . " l</td>";
            $verbrauchHTML .= "<td>" . $trip->averageElectricEngineConsumption / 1000 * $trip->mileage . " kWh</td>";

            //Verbrauch anhand historischer Spritpreise berechnen GetValueFloat(25028);
            $verbrauchHTML .= "<td>" . number_format((isset($trip->averageFuelConsumption) ?  $trip->averageFuelConsumption / 1000 * $trip->mileage * GetValueFloat(25028) : 0),2) . " €</td>";
            $verbrauchHTML .= "<td>" . number_format($trip->averageElectricEngineConsumption / 1000 * $trip->mileage * $Strompreis,2) . " €</td>";
            $verbrauchHTML .= "<td>" . number_format(((isset($trip->averageFuelConsumption) ?  $trip->averageFuelConsumption / 1000 * $trip->mileage * GetValueFloat(25028) : 0)) + ($trip->averageElectricEngineConsumption / 1000 * $trip->mileage * $Strompreis),2) . " €</td>";
            $verbrauchHTML .= "<td>" . number_format((((isset($trip->averageFuelConsumption) ?  $trip->averageFuelConsumption / 1000 * $trip->mileage * GetValueFloat(25028) : 0)) + ($trip->averageElectricEngineConsumption / 1000 * $trip->mileage * $Strompreis))/$trip->mileage,2) . " €</td></tr>";

            $shortTerm_mileage += $trip->mileage;
            $shortTerm_traveltime += $trip->traveltime;
            $shortTerm_averageSpeed += $trip->averageSpeed*$trip->mileage;
            $shortTerm_overallElectricEngineConsumption += $trip->averageElectricEngineConsumption/1000*$trip->mileage;
            $shortTerm_overallFuelConsumption += (isset($trip->averageFuelConsumption) ?  $trip->averageFuelConsumption/1000 * $trip->mileage : 0);    
            $shortTerm_averageElectricEngineConsumption += $trip->averageElectricEngineConsumption/10;
            $shortTerm_averageFuelConsumption += (isset($trip->averageFuelConsumption) ?  $trip->averageFuelConsumption/10 : 0);
        }


        $shortTerm_averageSpeed = $shortTerm_averageSpeed/$shortTerm_mileage;
        $shortTerm_averageFuelConsumption = $shortTerm_averageFuelConsumption/count($tripdatashortTerm);
        $shortTerm_averageElectricEngineConsumption = $shortTerm_averageElectricEngineConsumption/count($tripdatashortTerm);

        $tripdatashortTermHTML = '<html>
        <style>
        table {
        border-collapse: collapse;
        width: 100%;
        }

        th, td {
        text-align: left;
        padding: 8px;
        }

        tr:nth-child(even) {background-color: #3e5969;}
        </style>
        <table border = 0><tr align="center" style="overflow-x: auto;">
            <th>Datum</th>
            <th>Strecke</th>
            <th>Zeit</th>
            <th>km/h &#216</th>
            <th>l/100km</th>
            <th>kWh/100km</th>
            <th>l/Trip</th>
            <th>kWh/Trip</th>
            <th>Benzin/Trip</th>
            <th>Strom/Trip</th>
            <th>Gesamt/Trip</th>
            <th>Kosten/km</th>
            </tr>
            <tr align="center">
            <td>Longterm:</td>
            <td>'.$tripdatalongTerm->mileage.' km</td>
            <td>'.intdiv($tripdatalongTerm->traveltime, 60).':'. (strlen(($tripdatalongTerm->traveltime % 60)) == 1 ? '0':'') . ($tripdatalongTerm->traveltime % 60).' h</td>
            <td>'.number_format($tripdatalongTerm->averageSpeed,0).' km/h</td>
            <td>'.number_format($tripdatalongTerm->averageFuelConsumption/10,1,",",".").' l</td>
            <td>'.number_format($tripdatalongTerm->averageElectricEngineConsumption/10,0).' kWh</td>
            <td></td>
            <td></td>
            <td></td>
            <td></td>
            <td></td>
            <td></td>
            </tr>
            <tr align="center">
            <td>Gesamt:</td>
            <td>'.$shortTerm_mileage.' km</td>
            <td>'.intdiv($shortTerm_traveltime, 60).':'. (strlen(($shortTerm_traveltime % 60)) == 1 ? '0':'') . ($shortTerm_traveltime % 60).' h</td>
            <td>'.number_format($shortTerm_averageSpeed,0).' km/h</td>
            <td>'.number_format($shortTerm_averageFuelConsumption,1,",",".").' l</td>
            <td>'.number_format($shortTerm_averageElectricEngineConsumption,0).' kWh</td>
            <td>'.number_format($shortTerm_overallFuelConsumption, 0,",",".").' l</td>
            <td>'.number_format($shortTerm_overallElectricEngineConsumption, 1,",",".").' kWh</td>
            <td>'.number_format($shortTerm_overallFuelConsumption * 1.69, 2,",",".") .' €</td>
            <td>'.number_format($shortTerm_overallElectricEngineConsumption * $Strompreis,2,",",".") .' €</td>
            <td>'.number_format(($shortTerm_overallFuelConsumption * GetValueFloat(25028)) + ($shortTerm_overallElectricEngineConsumption * $Strompreis),2,",",".") .' €</td>
            <td>'.number_format((($shortTerm_overallFuelConsumption * GetValueFloat(25028)) + ($shortTerm_overallElectricEngineConsumption * $Strompreis)) / $shortTerm_mileage,2,",",".") .' €</td>
            </tr>'.$verbrauchHTML;
        $tripdatashortTermHTMLId = $this->CreateVariableByIdent(23309, 'tripdatashortTermHTML', 'tripdatashortTermHTML', 3, $profile = "~HTMLBox");
        SetValueString($tripdatashortTermHTMLId, $tripdatashortTermHTML);
        return [$tripdatashortTerm, $tripdatalongTerm];
    }    


    public function setCharger($action){
        curl_setopt_array($this->curl, array(
        CURLOPT_URL => self::API_HOST_AUDIETRON . '/vehicle/v1/vehicles/' . $this->vin . "/charging/mode",
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_ENCODING => "",
        CURLOPT_MAXREDIRS => 10,
        CURLOPT_TIMEOUT => 30,
        CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
        CURLOPT_CUSTOMREQUEST => "PUT",
        CURLOPT_POSTFIELDS => "{\n  \"preferredChargeMode\" : \"".$action."\"\n\n}",
        CURLOPT_HTTPHEADER => array(
            "cache-control: no-cache",
            "content-type: application/json",
            'authorization: Bearer '.$this->access_token,
        ),
        ));
        $response = curl_exec($this->curl);
        $err = curl_error($this->curl);
        if ($err) {
        return "cURL Error #:" . $err;
        } else {
        return $response;
        }
    }


    public function getTimersandProfiles(){
        curl_setopt($this->curl, CURLOPT_URL, self::API_HOST_AUDI . '/api/bs/departuretimer/v1/vehicles/' . $this->vin . '/timer/timersandprofiles');
        curl_setopt($this->curl, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($this->curl, CURLOPT_HTTPHEADER, array(
            'accept: application/json',
            'authorization: Bearer '.$this->vwaccess_token,
			'accept-charset: utf-8',
			'X-App-Version: 4.18.0',
			'X-App-Name: myAudi',
			'X-Client-Id: a09b50fe-27f9-410b-9a3e-cb7e5b7e45eb',
			"user-agent: Android/4.18.0 (Build 800239240.root project 'onetouch-android'.ext.buildTime) Android/11",
			'Host: mal-3a.prd.eu.dp.vwg-connect.com'
        ));
        $getTimersandProfiles = json_decode(curl_exec($this->curl));
        if($this->debug)print_r($getTimersandProfiles);        
        if (curl_errno($this->curl)) {
            $error_msg = curl_error($this->curl);
            return $error_msg;
        }

        if($this->responseCache){
            $getTimersandProfilesCacheId = $this->CreateVariableByIdent($_IPS['SELF'], '_getTimersandProfilesCache', 'AC_getTimersandProfilesCache', 3, $profile = "");
            SetValueString($getTimersandProfilesCacheId, json_encode($getTimersandProfiles));
        }

        return $getTimersandProfiles;
    }


    public function vehiclewakeup(){
        curl_setopt($this->curl, CURLOPT_URL, self::API_HOST_AUDIETRON . '/vehicle/v1/vehicles/' . $this->vin . '/vehiclewakeup');
        curl_setopt($this->curl, CURLOPT_CUSTOMREQUEST, "POST");
        curl_setopt($this->curl, CURLOPT_POSTFIELDS, '');
        curl_setopt($this->curl, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($this->curl, CURLOPT_HTTPHEADER, array(
            'accept: application/json',
            'authorization: Bearer '.$this->access_token,
			'accept-charset: utf-8',
			'X-App-Version: 4.18.0',
			'X-App-Name: myAudi',
			'X-Client-Id: a09b50fe-27f9-410b-9a3e-cb7e5b7e45eb',
			"user-agent: Android/4.18.0 (Build 800239240.root project 'onetouch-android'.ext.buildTime) Android/11",
			'Host: emea.bff.cariad.digital',
            'Content-Type: application/json; charset=utf-8'
        ));
        $response = curl_exec($this->curl);
        if (curl_errno($this->curl)) {
            $error_msg = curl_error($this->curl);
            return $error_msg;
        }
		//return json_decode($response);
        if($this->responseCache){
            $vehiclewakeupCacheId = $this->CreateVariableByIdent($_IPS['SELF'], '_vehiclewakeupCache', 'AC_vehiclewakeupCache', 3, $profile = "");
            SetValueString($vehiclewakeupCacheId, $response);
        }
    }    


    public function pendingrequest(){
    }


//####################################################################### Authentifizierungs-Funktionen ###############################################################################


    public function authenticate($authType){
        if (!$this->emailAddress || !$this->password) throw new \Exception("No email or password set");
        $this->fetchLogInForm($authType);
        $this->submitEmailAddressForm($authType);
        $this->submitPasswordForm($authType);
        $this->fetchInitialAccessTokens($authType);
    }


    private function fetchLogInForm($authType){
        $this->state = self::generateMockUuid();
        $PKCEPair = $this->generatePKCEPair();

        $this->codeChallenge = $PKCEPair['codeChallenge'];
        $this->codeVerifier = $PKCEPair['codeVerifier'];

        switch ($authType) {
            case 'audietron':
                $url = self::AUTH_HOST . '/oidc/v1/authorize?redirect_uri=myaudi%3A%2F%2F%2F&scope=openid&prompt=login&code_challenge='.$this->codeChallenge.'&state=' . $this->state . '&response_type=code&code_challenge_method=S256&client_id=' . self::APP_CLIENT_ID;
                break;
            case 'audi':
                $this->nonce = self::generateNonce();
                $url = self::AUTH_HOST . '/oidc/v1/authorize?client_id=' . self::APP_CLIENT_ID . '&scope=address%20profile%20badge%20birthdate%20birthplace%20nationalIdentifier%20nationality%20profession%20email%20vin%20phone%20nickname%20name%20picture%20mbb%20gallery%20openid&response_type=token%20id_token&redirect_uri=myaudi:///&nonce='.$this->nonce.'&state=' . $this->state . '&ui_locales=de-DE%20de&prompt=login';
                break;
        }

        $res = $this->client->request('GET', $url, ['cookies' => $this->clientCookieJar]);

        $body = strval($res->getBody());
        $body = preg_replace('#<script(.*?)>(.*?)</script>#is', '', $body);

        $xml = new SimpleXMLElement($body);

        $csrfQuery = $xml->xpath("//*[@name='_csrf']/@value");
        $relayStateQuery = $xml->xpath("//*[@name='relayState']/@value");
        $hmacQuery = $xml->xpath("//*[@name='hmac']/@value");
        $formActionQuery = $xml->xpath("//*[@name='emailPasswordForm']/@action");

        if (!$csrfQuery || !$relayStateQuery || !$hmacQuery || !$formActionQuery)throw new \Exception('Gesuchte Werte nicht gefunden im ersten Schritt...');

        $this->csrf = strval($csrfQuery[0][0][0]);
        $this->relayState = strval($relayStateQuery[0][0][0]);
        $this->hmac = strval($hmacQuery[0][0][0]);
        $this->nextFormAction = strval($formActionQuery[0][0][0]);
    }


    private function submitEmailAddressForm($authType){
        $url = self::AUTH_HOST . $this->nextFormAction;

        $res =	$this->client->request('POST', $url, 
            [
                'cookies' => $this->clientCookieJar,
                'headers' => [
                    'user-agent' => self::AUTH_USER_AGENT_SPOOF,
                    'content-type' => 'application/x-www-form-urlencoded',
                    'accept-language' => 'en-US,en;q=0.9',
                    'accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3',
                    'Accept-Encoding' => 'gzip, deflate',
                    'x-requested-with' => 'de.myaudi.mobile.assistant',
                ],
                'form_params' => [
                    '_csrf' => $this->csrf,
                    'relayState' => $this->relayState,
                    'hmac' => $this->hmac,
                    'email' => $this->emailAddress
                ],
                'gzip' => 'true',
                'followAllRedirects' => 'true',
            ]
        );

        $returnedHtml = strval($res->getBody());

        $hmacQuery = explode('hmac":"',$returnedHtml);
        $hmacQuery = explode('","',$hmacQuery[1])[0];

        $this->hmac = strval($hmacQuery);
    }


   private function submitPasswordForm($authType){
        $url = self::AUTH_HOST . "/signin-service/v1/" . self::APP_CLIENT_ID . "/login/authenticate";
        try {
            $res =	$this->client->request('POST', $url, 
                [
                    'cookies' => $this->clientCookieJar,
                    'headers' => [
                        'user-agent' => self::AUTH_USER_AGENT_SPOOF,
                        'content-type' => 'application/x-www-form-urlencoded',
                        'accept-language' => 'en-US,en;q=0.9',
                        'accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3',
                        'Accept-Encoding' => 'gzip, deflate',
                        'x-requested-with' => 'de.myaudi.mobile.assistant',
                        ],
                    'form_params' => [
                        '_csrf' => $this->csrf,
                        'relayState' => $this->relayState,
                        'hmac' => $this->hmac,
                        'email' => $this->emailAddress,
                        'password' => $this->password
                    ]
                ]
            );
            if($this->debug)print_r($res);
        } catch (\Exception $e) {
            if($this->debug)print_r($e);
            // We are expecting this to throw an exception even when succesful, 
            // because guzzle seems to have no way of gracefully handling redirects that 
            // attempt to redirect to a custom uri scheme (i.e. car-net:/// )

            // Luckily guzzle returns the failed uri in the exception message, so we 
            // can just grab the value we need from that and move on...
            switch ($authType) {
                case 'audietron':
                    $this->code = trim(explode("&code=", $e->getMessage())[1]);
                    if (empty($this->code) || preg_match('/^[a-f0-9]{96}$/', $this->code) === false) 
                        throw new \Exception('No "code" value was returned from the API after the final step of the log in process. This is most likely due to an incorrect email address or password.');
                    break;
                case 'audi':
                    $this->vwid_token = @trim(explode("&id_token=", $e->getMessage())[1]);
                    if (empty($this->vwid_token)) 
                        throw new \Exception('No "id_token" value was returned from the API after the final step of the log in process. This is most likely due to an incorrect email address or password.');
                    break;
            }
        }
    }


    private function fetchInitialAccessTokens($authType){
        switch ($authType) {
            case 'audietron':
                if (!$this->code || !$this->codeVerifier)
                    throw new \Exception("Can not request access tokens without valid 'code' and 'codeVerifier' values.");
                $url = self::API_HOST_AUDIETRON . '/login/v1/idk/token';
                $params = [
                    'headers' => [
                        'user-agent' => self::APP_USER_AGENT_SPOOF,
                        'content-type' => 'application/x-www-form-urlencoded',
                        'accept-language' => 'en-us',
                        'accept' => '*/*',
                        'accept-encoding' => 'gzip, deflate, br',
                    ],
                    'form_params' => [
                        'grant_type' => 'authorization_code',
                        'code' => $this->code,
                        'client_id' => self::APP_CLIENT_ID,
                        'redirect_uri' => 'myaudi:///',
                        'code_verifier' => $this->codeVerifier
                    ]
                ];
                break;
            case 'audi':
                if (!$this->vwid_token )
                    throw new \Exception("Can not request access tokens without valid 'id_token' value.");
                $url= "https://mbboauth-1d.prd.ece.vwg-connect.com/mbbcoauth/mobile/oauth2/v1/token";
                $params = [
                    'headers' => [
                        'user-agent' => self::APP_USER_AGENT_SPOOF,
                        'X-App-Version' => '3.22.0',
                        'X-App-Name' => 'myAudi',
                        'X-Client-Id' => '77869e21-e30a-4a92-b016-48ab7d3db1d8',
                        'Host' => 'mbboauth-1d.prd.ece.vwg-connect.com',
                        'accept-encoding' => 'gzip, deflate',
                    ],
                    'form_params' => [
                            'grant_type' => "id_token",
                            'token' => $this->vwid_token,
                            'scope' => "sc2:fal",
                    ],
                    'gzip' => 'true',
                    'followAllRedirects' => 'true',
                    'cookies' => $this->clientCookieJar,
                ];
                break;
        }

        $res =	$this->client->request('POST', $url, $params);

        $responseJson = json_decode(strval($res->getBody()), true); 

        switch ($authType) {
            case 'audietron':
                $this->access_token = $responseJson['access_token'];
                $this->token_expires = time() + $responseJson['expires_in'];
                $this->id_token = $responseJson['id_token'];
                $this->refresh_token = $responseJson['refresh_token'];
                $responseJson['token_expires'] = time() + $responseJson['expires_in'];
                $this->setTokens($responseJson);
                
                break;
            case 'audi':
                $this->vwaccess_token = $responseJson['access_token'];
                $this->vwtoken_expires = time() + $responseJson['expires_in'];
                $this->vwrefresh_token = $responseJson['refresh_token'];
                $responseJson = [
                    'vwaccess_token' => $this->vwaccess_token, 
                    'vwtoken_expires' => $this->vwtoken_expires,
                    'vwrefresh_token' => $this->vwrefresh_token,
                    'vwtoken_expires_in' => $responseJson['expires_in']

                ];
                $this->setTokens($responseJson);
                break;
        }

        if (is_callable($this->saveCallback))
            call_user_func($this->saveCallback, $this); 
    }


    public function refreshTokens($authType){
        switch ($authType) {
            case 'audietron':
                if (!$this->refresh_token)throw new \Exception("Can not refresh access tokens without a valid 'refresh token' value.");
                if (time() >= $this->token_expires)throw new \Exception("Your saved refresh token has expired. Please re-authenticate.");            
                $url = self::API_HOST_AUDIETRON . '/login/v1/idk/token';
                $params = [	
                'headers' => [
                    'user-agent' => self::APP_USER_AGENT_SPOOF,
                    'content-type' => 'application/x-www-form-urlencoded',
                    'accept-language' => 'en-us',
                    'accept' => '*/*',
                    'accept-encoding' => 'gzip, deflate, br'
                ],
                'form_params' => [
                    'grant_type' => 'refresh_token',
                    'refresh_token' => $this->refresh_token,
                    'client_id' => self::APP_CLIENT_ID,
                    'code_verifier' => $this->codeVerifier
                ]
            ];
                break;
            case 'audi':
                if (!$this->vwrefresh_token)throw new \Exception("Can not refresh access tokens without a valid 'vwrefresh token' value.");
                if (time() >= $this->token_expires)throw new \Exception("Your saved vwrefresh token has expired. Please re-authenticate.");            
                $url= "https://mbboauth-1d.prd.ece.vwg-connect.com/mbbcoauth/mobile/oauth2/v1/token";
                $params = [	
                'headers' => [
                    'user-agent' => 'ioBrokerv0.0.72',
                    'X-App-Version' => '3.22.0',
                    'X-App-Name' => 'myAudi',
                    'X-Client-Id' => '77869e21-e30a-4a92-b016-48ab7d3db1d8',
                    'Host' => 'mbboauth-1d.prd.ece.vwg-connect.com',
                    'accept-encoding' => 'gzip, deflate',
                ],
                'form_params' => [
                    'grant_type' => 'refresh_token',
                    'token' => $this->vwrefresh_token,
                    'scope' => "sc2:fal",
                ],
                'gzip' => 'true',
                'followAllRedirects' => 'true',
                'cookies' => $this->clientCookieJar,
            ];
                break;
        }

        $res =	$this->client->request('POST', $url, $params);

        $responseJson = json_decode(strval($res->getBody()), true); 

        if($this->debug)print_r($responseJson);

        switch ($authType) {
            case 'audietron':
                $this->access_token = $responseJson['access_token'];
                $this->token_expires = time() + $responseJson['expires_in'];
                $this->id_token = $responseJson['id_token'];
                $this->refresh_token = $responseJson['refresh_token'];
                $responseJson['token_expires'] = time() + $responseJson['expires_in'];
                $this->setTokens($responseJson);
                break;
            case 'audi':
                $this->vwaccess_token = $responseJson['access_token'];
                $this->vwtoken_expires = time() + $responseJson['expires_in'];
                $this->vwrefresh_token = isset($responseJson['refresh_token'])?$responseJson['refresh_token']:'';
                $responseJson = [
                    'vwaccess_token' => $this->vwaccess_token, 
                    'vwtoken_expires' => $this->vwtoken_expires,
                    'vwrefresh_token' => $this->vwrefresh_token,
                ];
                $this->setTokens($responseJson);
                break;
        }
        if (is_callable($this->saveCallback))
            call_user_func($this->saveCallback, $this); 
    }


//####################################################################### Helper-Funktionen ###############################################################################


    public function getTokens(){
        $this->token_expires = (int)@GetValueString(IPS_GetObjectIDByName('token_expires',$this->rootId));
        $this->refresh_token = @GetValueString(IPS_GetObjectIDByName('refresh_token',$this->rootId));
        $this->access_token = @GetValueString(IPS_GetObjectIDByName('access_token',$this->rootId));
        $this->id_token = @GetValueString(IPS_GetObjectIDByName('id_token',$this->rootId));
        $this->vwtoken_expires = (int)@GetValueString(IPS_GetObjectIDByName('vwtoken_expires',$this->rootId));
        $this->vwrefresh_token = @GetValueString(IPS_GetObjectIDByName('vwrefresh_token',$this->rootId));
        $this->vwaccess_token = @GetValueString(IPS_GetObjectIDByName('vwaccess_token',$this->rootId));

        if(empty($this->token_expires) || empty($this->refresh_token) || empty($this->access_token) || empty($this->id_token)){
            $this->authenticate('audietron');
        }
        if(time() > $this->token_expires)$this->authenticate('audietron');
        else $this->refreshTokens('audietron');

        if(empty($this->vwtoken_expires) || empty($this->vwrefresh_token) || empty($this->vwaccess_token)){
            $this->authenticate('audi');
        }
        if(time() > $this->vwtoken_expires)$this->authenticate('audi');
        else $this->refreshTokens('audi');
    }


    private function setTokens($responseJson){
        foreach($responseJson as $name => $value){
            $gettokenId = $this->CreateVariableByIdent($this->rootId, $name, $name, 3, $profile = "");
            SetValueString($gettokenId, $value);
        }
    }      


    public function getAllAuthenticationTokens(){	
        return [
            'access_token' => $this->access_token, 
            'id_token' => $this->id_token,
            'token_expires' => $this->token_expires,
            'refresh_token' => $this->refresh_token,
            'vwaccess_token' => $this->vwaccess_token, 
            'vwrefresh_token' => $this->vwrefresh_token,
            'vwtoken_expires' => $this->vwtoken_expires,
            'codeVerifier' => $this->codeVerifier,
            'emailAddress' => $this->emailAddress,
        ];
    }      


    private function CreateVariableByIdent($parentId, $ident, $name, $type, $profile = ""){
        $vid = @IPS_GetObjectIDByIdent("AC_".IPS_GetName($parentId).$ident, $parentId);
        if($vid === false) {
            $vid = IPS_CreateVariable($type);
            IPS_SetParent($vid, $parentId);
            IPS_SetName($vid, $name);
            IPS_SetIdent($vid, "AC_".IPS_GetName($parentId).$ident);
            if($profile != "")IPS_SetVariableCustomProfile($vid, $profile);
        }
        return $vid;
    }


    private function generateMockUuid(){
        // This is derived from https://www.php.net/manual/en/function.uniqid.php#94959
        // This method doesn't create unique values or cryptographically secure values. 
        // It simply creates mocks to satisfy the Car-Net APIs expectations. 

        return sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
            mt_rand(0, 0xffff), mt_rand(0, 0xffff),
            mt_rand(0, 0xffff),
            mt_rand(0, 0x0fff) | 0x4000,
            mt_rand(0, 0x3fff) | 0x8000,
            mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff)
        );
    }


    private function generatePKCEPair(){
        $bytes = random_bytes(64 / 2);
        $codeVerifier = bin2hex($bytes);

        $hashOfVerifier = hash('sha256', $codeVerifier, true);
        $codeChallenge = strtr(base64_encode($hashOfVerifier), '+/', '-_'); 

        return [
            'codeVerifier' => $codeVerifier, 
            'codeChallenge' => $codeChallenge
        ];
    }


    private function generateNonce(){
        $s = (string)intval(microtime(true) * 1000);
        $nonce = base64_encode(hash("sha256", $s, True));
        $nonce = substr($nonce, 0, -1);
        return $nonce;
    }
}
4 „Gefällt mir“

Super, werde mich demnächst mal damit beschäftigen. Wollte unseren Q3 schon lange mal einbinden.

Das wird bestimmt spannend. Vielleicht kann man es dann in ein Modul gießen.

3 „Gefällt mir“

Schon jemand damit was zum Laufen bekommen?
Ein Modul stelle ich mir relativ schwierig vor, da sich die API-Endpunkte je nach Fahrzeug und Ausstattung doch deutlich unterscheiden. Um das für alle Marken des VW-Konzerns und für die dazugehörigen Typen umzusetzen wäre eine ganze Menge an Arbeit und Schwarmwissen zu investieren…

Ich hab’s noch nicht versucht, mich da einzuarbeiten dürfte doch etwas Zeit kosten. Das wird vielleicht ein Projekt für lange Winterabende :slight_smile:

1 „Gefällt mir“

Audi hat mal wieder an der API gebastelt…
In der getStatus-Funktion muss eine Zeile angepasst werden, das jobs=all wird wohl nicht mehr unterstützt:

        //curl_setopt($this->curl, CURLOPT_URL, self::API_HOST_AUDIETRON . '/vehicle/v1/vehicles/' . $this->vin . '/selectivestatus?jobs=all');
        curl_setopt($this->curl, CURLOPT_URL, self::API_HOST_AUDIETRON . '/vehicle/v1/vehicles/' . $this->vin . '/selectivestatus?jobs=charging%2CchargingTimers%2CchargingProfiles%2CfuelStatus%2Cmeasurements%2CoilLevel%2CvehicleHealthInspection%2Caccess%2CvehicleLights');