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 vw-car-net-api/README.md at master · thomasesmith/vw-car-net-api · GitHub 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());
//print_r($audi->getParkingposition());
//print_r($audi->getTripdata('shortTerm'));
//print_r($audi->getTripdata('longTerm'));
//print_r($audi->getTripDataHTML());
//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->getTimersandProfiles());
//print_r($audi->setTimersandProfiles()); //Todo um Timer im Auto zu setzen
//print_r($audi->servicebook());
//print_r($audi->destinations());
//print_r($audi->honkandflash('flash')); //'flash' oder 'honkandflash'


//print_r($audi->getAllAuthenticationTokens());
//print_r($audi->authenticate('audi'));
//print_r($audi->fetchInitialAccessTokens('audi'));

//ToDo vehicleWakeup-Flow erstellen
//print_r($audi->vehiclewakeup());
//print_r($audi->vehiclehealthwakeup('7fc27d13-d5a3-4adc-8fe6-589c5cd1d740'));
//print_r($audi->pendingrequests());


class audi{


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


    private $emailAddress = "widmayer@entropit.de";     //myAudi-Login Mailadresse
    private $password = "Easy4711!";                    //myAudi-Login Kennwort
    private $securityPIN = "4711";                      //PIN für sicherheitsrelevante Funktionen, wird noch nicht verwendet.
    private $vin = "WAUZZZGY9NA006343";                 //FIN des Audi
    private $rootId = 23309;                       //Startkategorie, unterhalb der alle Objekte erstellt werden
    private $tripDataMax = 999999;                        //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'
    private $retrycounter = 0;
    
    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';

    const APP_USER_AGENT = 'Android/5.1.0 (Build 800343228.2512082054) Android/12';
    const APP_VERSION = '5.1.0';
    const APP_NAME = 'myAudi';




	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);
        }

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

//ToDo cURL-Header anpassen
    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);
            }

/* Macht zu viele Dummy-Instanzen
            $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;
        }
	}

//ToDo HTML-Datei unter tripData erstellen
    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($this->rootId, 'tripdatashortTermHTML', 'tripdatashortTermHTML', 3, $profile = "~HTMLBox");
        SetValueString($tripdatashortTermHTMLId, $tripdatashortTermHTML);
        return [$tripdatashortTerm, $tripdatalongTerm];
    }  

//ToDo cUrl Header    
    public function getClimater()
    {

                // 🔁 FAL-Token prüfen
        if (
            empty($this->vwaccess_token) ||
            empty($this->vwtoken_expires) ||
            time() >= $this->vwtoken_expires - 60
        ) {
            $this->ensureIdentityValid();
            $this->fetchInitialAccessTokens('audi');
        }
        $url = 'https://mal-3a.prd.eu.dp.vwg-connect.com/api/bs/climatisation/v1/vehicles/'
            . $this->vin . '/climater';

        $res = $this->client->request('GET', $url, [
            'headers' => [
                'Accept'          => 'application/json',
                'Accept-Charset'  => 'utf-8',
                'Authorization'   => 'Bearer ' . $this->vwaccess_token,
                'X-App-Name'      => 'myAudi',
                'X-App-Version'   => '5.1.0',
                'X-Client-ID'     => '79315a01-be62-4024-8347-a3e950e9d3ad',
                'User-Agent'      => 'Android/5.1.0 (Build 800343228.2512082054) Android/12',
            ],
            'http_errors' => false,
        ]);

        $status = $res->getStatusCode();
        $body   = (string)$res->getBody();
        $getClimater   = json_decode($body);

        if ($status === 401) {
            $this->fetchInitialAccessTokens('audi');
            return $this->getClimater();
        }

        if ($status !== 200) {
            throw new Exception("Climater HTTP {$status}:\n{$body}");
        }

        if (!isset($getClimater->climater)) {
            throw new Exception(
                "No climater object returned:\n{$body}"
            );
        }

        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);
            }

            if(isset($getClimater->climater->status->vehicleParkingClockStatusData)){
                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;
    }

//ToDo cUrl Header 
    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 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;
        }
    }

//ToDo cUrl Header 
    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 setTimersandProfiles(){} //Platzhalter


    public function servicebook(){
        curl_reset($this->curl);
        curl_setopt_array($this->curl, [
            CURLOPT_URL => 'https://emea.bff.cariad.digital/login/v1/audi/token',
            CURLOPT_CUSTOMREQUEST => 'POST',
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_POSTFIELDS => json_encode([
                'token'      => $this->id_token,
                'grant_type' => 'id_token',
                'stage'      => 'live',
                'config'     => 'myaudi'
            ]),
            CURLOPT_HTTPHEADER => [
                'accept: application/json',
                'content-type: application/json; charset=utf-8',
                'authorization: Bearer ' . $this->id_token,
                'x-market: de_DE',
                'X-App-Version:' => self::APP_VERSION,
                'X-App-Name:' => self::APP_NAME,
                'X-Client-Id:' => self::APP_CLIENT_ID,
                'User-Agent:' => self::APP_USER_AGENT,
            ],
        ]);

        $tokenResponse = json_decode(curl_exec($this->curl));
        if (curl_errno($this->curl)) {
            return curl_error($this->curl);
        }

        if (empty($tokenResponse->access_token)) {
            throw new Exception('Servicebook token exchange failed');
        }
        $serviceAccessToken = $tokenResponse->access_token;

        /**
        * 2️⃣ Servicebook-Liste abrufen
        */
        curl_reset($this->curl);
        curl_setopt_array($this->curl, [
            CURLOPT_URL => 'https://app-api.my.audi.com/sbs/v1/vehicles/' . $this->vin . '/service-book',
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_HTTPHEADER => [
                'accept: application/json',
                'authorization: Bearer ' . $serviceAccessToken,
                'x-market: de_DE',
                'X-App-Version:' => self::APP_VERSION,
                'X-App-Name:' => self::APP_NAME,
                'X-Client-Id:' => self::APP_CLIENT_ID,
                'User-Agent:' => self::APP_USER_AGENT,
            ],
        ]);

        $servicebook = json_decode(curl_exec($this->curl));
        if (curl_errno($this->curl)) {
            return curl_error($this->curl);
        }

        if ($this->responseCache) {
            $cacheId = $this->CreateVariableByIdent($_IPS['SELF'], '_servicebookCache', 'AC_servicebookCache', 3);
            SetValueString($cacheId, json_encode($servicebook));
        }

        /**
        * 3️⃣ Details pro Service-Eintrag
        */
        $InstanzID = @IPS_GetInstanceIDByName('servicebook', $this->rootId);
        if ($InstanzID === false) {
            $InstanzID = IPS_CreateInstance("{485D0419-BE97-4548-AA9C-C083EB82E61E}");
            IPS_SetName($InstanzID, 'servicebook');
            IPS_SetParent($InstanzID, $this->rootId);
            IPS_ApplyChanges($InstanzID);
        }

        foreach ($servicebook as $service) {

            curl_reset($this->curl);
            curl_setopt_array($this->curl, [
                CURLOPT_URL => 'https://app-api.my.audi.com/sbs/v1/vehicles/' . $this->vin . '/service-book/' . $service->serviceDocumentId,
                CURLOPT_RETURNTRANSFER => true,
                CURLOPT_HTTPHEADER => [
                    'accept: application/json',
                    'authorization: Bearer ' . $serviceAccessToken,
                    'x-market: de_DE',
                    'X-App-Version:' => self::APP_VERSION,
                    'X-App-Name:' => self::APP_NAME,
                    'X-Client-Id:' => self::APP_CLIENT_ID,
                    'User-Agent:' => self::APP_USER_AGENT,
                ],
            ]);

            $serviceDocument = json_decode(curl_exec($this->curl));
            if (curl_errno($this->curl)) {
                return curl_error($this->curl);
            }
        
            $serviceID = @IPS_GetInstanceIDByName(date("Y-m-d",strtotime($serviceDocument->serviceDate)) .' '. $serviceDocument->jobDescription .' '. $serviceDocument->odometerValue.$serviceDocument->odometerUnit .' '. $serviceDocument->dealer->name, $InstanzID);
            if ($serviceID === false){
                $serviceID = IPS_CreateInstance("{485D0419-BE97-4548-AA9C-C083EB82E61E}");
                IPS_SetName($serviceID, date("Y-m-d",strtotime($serviceDocument->serviceDate)) .' '. $serviceDocument->jobDescription .' '. $serviceDocument->odometerValue.$serviceDocument->odometerUnit .' '. $serviceDocument->dealer->name);
                IPS_SetParent($serviceID, $InstanzID);
                IPS_ApplyChanges($serviceID);
            }

            $mobilityGuarantee = $this->CreateVariableByIdentsb($serviceID, 'mobilityGuarantee', 'mobilityGuarantee', 0, $profile = "");
            if(GetValue($mobilityGuarantee) != $serviceDocument->mobilityGuarantee)SetValue($mobilityGuarantee,$serviceDocument->mobilityGuarantee);

            if(isset($serviceDocument->additionalNote) && !empty($serviceDocument->additionalNote)){
                $additionalNote = $this->CreateVariableByIdentsb($serviceID, 'additionalNote', 'additionalNote', 3, $profile = "");
                if(GetValue($additionalNote) != $serviceDocument->additionalNote)SetValue($additionalNote,$serviceDocument->additionalNote);
            }

            if(isset($serviceDocument->tasksList) && !empty($serviceDocument->tasksList)){
                $tasklistID = @IPS_GetInstanceIDByName('tasklist', $serviceID);
                if ($tasklistID === false){
                    $tasklistID = IPS_CreateInstance("{485D0419-BE97-4548-AA9C-C083EB82E61E}");
                    IPS_SetName($tasklistID, 'tasklist');
                    IPS_SetParent($tasklistID, $serviceID);
                    IPS_ApplyChanges($tasklistID);
                }

                foreach($serviceDocument->tasksList as $key => $task){
                    $taskID = $this->CreateVariableByIdentsb($tasklistID, 'task'.$key, 'task'.$key, 3, $profile = "");
                    if(GetValue($taskID) != $task->description)SetValue($taskID,$task->description);
                }
             }
        }
    }

//ToDo HTML unter tripData erstellen lassen
    public function destinations()
    {
        $this->curlInit(
            'https://emea.bff.cariad.digital/navigation/v1/navigation/destinations',
            [
                'accept: application/json',
                'authorization: Bearer ' . $this->access_token,
            ]
        );

        $response = curl_exec($this->curl);

        if (curl_errno($this->curl)) {
            return curl_error($this->curl);
        }
        $htmlTable = $this->destinationObjectToHtmlTable(json_decode($response));
        $destinationsHTMLId = $this->CreateVariableByIdent($this->rootId, 'destinationsHTML', 'destinationsHTML', 3, $profile = "~HTMLBox");
        SetValueString($destinationsHTMLId, $htmlTable);
        return json_decode($response);
    }





//ToDo cUrl Header 
    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.22.0',
			'X-App-Name: myAudi',
			'X-Client-Id: a09b50fe-27f9-410b-9a3e-cb7e5b7e45eb',
			"user-agent: Android/4.22.0 (Build 800239930.root project 'onetouch-android'.ext.buildTime) Android/11",
			'Host: emea.bff.cariad.digital',
            'Content-Type: application/json; charset=utf-8'
        ));
        $response = json_decode(curl_exec($this->curl));
        if (curl_errno($this->curl)) {
            $error_msg = curl_error($this->curl);
            return $error_msg;
        }
		
        if($this->responseCache){
            $vehiclewakeupCacheId = $this->CreateVariableByIdent($_IPS['SELF'], '_vehiclewakeupCache', 'AC_vehiclewakeupCache', 3, $profile = "");
            SetValueString($vehiclewakeupCacheId, json_encode($response));
        }
        $requestID = $response->data->requestID;
        $this->pendingrequests($requestID);
        return $requestID;
    }    

//ToDo cUrl Header 
        public function vehiclehealthwakeup($requestID = ''){
        curl_reset($this->curl);
        curl_setopt($this->curl, CURLOPT_URL, self::API_HOST_AUDIETRON . '/vehicle/v1/vehicles/' . $this->vin . '/vehiclehealthwakeup/' . $requestID);
        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.22.0',
			'X-App-Name: myAudi',
			'X-Client-Id: a09b50fe-27f9-410b-9a3e-cb7e5b7e45eb',
			"user-agent: Android/4.22.0 (Build 800239930.root project 'onetouch-android'.ext.buildTime) Android/11",
			'Host: emea.bff.cariad.digital'
        ));
        $getvehiclehealthwakeup = json_decode(curl_exec($this->curl));
        //$getvehiclehealthwakeup = json_decode(GetValue(49321));
        if($this->debug)print_r($getvehiclehealthwakeup);        
        if (curl_errno($this->curl)) {
            $error_msg = curl_error($this->curl);
            return $error_msg;
        }

        if($this->responseCache){
            $getVehicleHealthwakeupCacheId = $this->CreateVariableByIdent($_IPS['SELF'], '_getVehicleHealthwakeupCache', 'AC_getVehicleHealthwakeupCache', 3, $profile = "");
            SetValueString($getVehicleHealthwakeupCacheId, json_encode($getvehiclehealthwakeup));
        }

        if(isset($getvehiclehealthwakeup->data->warningLights) && $getvehiclehealthwakeup->data->warningLights[0]->text != 'Fuel reserve warning light on'){
            $html='';
            foreach($getvehiclehealthwakeup->data->warningLights as $warningLight){
                $html .= '<tr align="center"><td>'  . ($warningLight->icon == 'ICON_NOT_FOUND' ? ($warningLight->iconName == 'ICON_NOT_FOUND' ? '':$warningLight->iconName ) :  '<img src="'.$warningLight->icon.'" alt="'.$warningLight->iconName.'" />') . "</td>";
                $html .= "<td>" . $warningLight->text . "</td>";
            }
            $html = '<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>'.$html;


        SetValueString(51913,$html);
        IPS_SetHidden(24420, false);
        SetValueBoolean(15304, true);

        }
        else{
            SetValueString(51913,"");
            IPS_SetHidden(24420, true);  
            SetValueBoolean(15304, false);
        } 

        return $getvehiclehealthwakeup;   
    }    

    public function pendingrequests()
    {
        $this->curlInit(
            self::API_HOST_AUDIETRON . '/vehicle/v1/vehicles/' . $this->vin . '/pendingrequests',
            [
                'accept: application/json',
                'authorization: Bearer ' . $this->access_token,
            ]
        );

        $getpendingrequests = curl_exec($this->curl);

        if (curl_errno($this->curl)) {
            return curl_error($this->curl);
        }

        if($this->responseCache){
            $getPendingRequestsCacheId = $this->CreateVariableByIdent($_IPS['SELF'], '_getPendingRequestsCache', 'AC_getPendingRequestsCache', 3, $profile = "");
            SetValueString($getPendingRequestsCacheId, json_encode($getpendingrequests));
        }

        if(!isset($getpendingrequests->data) || empty($getpendingrequests->data)){
            if(GetValueInteger(58360) < 5){
                IPS_SetScriptTimer(58431, 10);
                SetValueInteger(58360, GetValueInteger(58360)+1);
            }
            else{
                IPS_SetScriptTimer(58431, 0);
                SetValueInteger(58360, 0);
            }
        }
        else{
            IPS_SetScriptTimer(58431, 0);
            SetValueInteger(58360, 0);
            foreach($getpendingrequests->data as $request){
                if($request->id == $requestID){
                    if($request->status == 'in_progress')$this->pendingrequests($requestID);
                    elseif($request->status == 'successful')$this->vehiclehealthwakeup($requestID);
                }
            }
        }

        return $getpendingrequests;    
    }


    public function honkandflash($action = "flash"){ //"flash" oder "honkandflash"
        curl_setopt_array($this->curl, array(
        CURLOPT_URL => self::API_HOST_AUDIETRON . '/vehicle/v1/vehicles/' . $this->vin . "/honkandflash",
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_ENCODING => "",
        CURLOPT_MAXREDIRS => 10,
        CURLOPT_TIMEOUT => 30,
        CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
        CURLOPT_CUSTOMREQUEST => "POST",
        CURLOPT_POSTFIELDS => "{\n  \"duration_s\" : 10,\n  \"mode\" : \"".$action."\",\n  \"userPosition\" : {\n    \"latitude\" : ".GetValue(44817).",\n    \"longitude\" : ".GetValue(39440)."\n  }\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;
        };
    }


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


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

        // 1️⃣ Identity Login (audietron)
        $this->fetchLogInForm('audietron');
        $this->submitEmailAddressForm('audietron');
        $this->submitPasswordForm('audietron');
        $this->fetchInitialAccessTokens('audietron');

        // 2️⃣ FAL Token (audi) – DAS FEHLTE
        $this->fetchInitialAccessTokens('audi');
    }



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

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

        if ($authType !== 'audietron') {
            throw new Exception('Only audietron supported here');
        }

        // 🔥 NUR Authorization Code Flow
        $url = self::AUTH_HOST . '/oidc/v1/authorize?' . http_build_query([
            'response_type'         => 'code',
            'client_id'             => self::APP_CLIENT_ID,
            'redirect_uri'          => 'myaudi:///',
            'scope'                 => 'openid profile mbb',
            'prompt'                => 'login',
            'state'                 => $this->state,
            'code_challenge'        => $this->codeChallenge,
            'code_challenge_method' => 'S256',
        ]);

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

        $html = (string)$res->getBody();
        $html = preg_replace('#<script[^>]*>.*?</script>#is', '', $html);

        $xml = new SimpleXMLElement($html);

        $this->csrf           = (string)$xml->xpath("//*[@name='_csrf']/@value")[0];
        $this->relayState     = (string)$xml->xpath("//*[@name='relayState']/@value")[0];
        $this->hmac           = (string)$xml->xpath("//*[@name='hmac']/@value")[0];
        $this->nextFormAction = (string)$xml->xpath("//*[@name='emailPasswordForm']/@action")[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);
    }

//ToDo cUrl Header 
    private function submitPasswordForm($authType)
    {
        if ($authType !== 'audietron') {
            throw new Exception('Only audietron supported');
        }

        $url = self::AUTH_HOST . "/signin-service/v1/" . self::APP_CLIENT_ID . "/login/authenticate";

        $res = $this->client->request('POST', $url, [
            'cookies' => $this->clientCookieJar,
            'allow_redirects' => false,
            'headers' => [
                'User-Agent'   => self::AUTH_USER_AGENT_SPOOF,
                'Content-Type' => 'application/x-www-form-urlencoded',
                'Accept'       => 'text/html',
            ],
            'form_params' => [
                '_csrf'      => $this->csrf,
                'relayState' => $this->relayState,
                'hmac'       => $this->hmac,
                'email'      => $this->emailAddress,
                'password'   => $this->password,
            ],
        ]);

        if (!$res->hasHeader('Location')) {
            throw new Exception('Password submit did not return redirect');
        }

        $nextUrl = $res->getHeaderLine('Location');

        // Redirect-Kette verfolgen
        $finalUrl = $this->followRedirectsUntilMyaudi($nextUrl);

        // 🔥 WICHTIG: NICHT parse_url() benutzen!
        if (!preg_match('/[?&]code=([^&]+)/', $finalUrl, $m)) {
            throw new Exception("No authorization code returned:\n{$finalUrl}");
        }

        $this->code = $m[1];

        if ($this->debug) {
            echo "[AUTH] Authorization code extracted (length=" . strlen($this->code) . ")\n";
        }
    }

//ToDo cUrl Header 
    private function followRedirectsUntilMyaudi(string $startUrl): string
    {
        $url = $startUrl;

        for ($i = 0; $i < 20; $i++) {

            if (!preg_match('#^https?://#', $url) && !str_starts_with($url, 'myaudi://')) {
                $url = self::AUTH_HOST . '/' . ltrim($url, '/');
            }

            $res = $this->client->request('GET', $url, [
                'cookies' => $this->clientCookieJar,
                'allow_redirects' => false,
                'headers' => [
                    'User-Agent' => self::AUTH_USER_AGENT_SPOOF,
                    'Accept'     => 'text/html',
                ],
            ]);

            if ($res->hasHeader('Location')) {
                $location = $res->getHeaderLine('Location');

                if (str_starts_with($location, 'myaudi://')) {
                    return $location;
                }

                $url = $location;
                continue;
            }

            $html = (string)$res->getBody();

            if (preg_match('#myaudi://[^"\']+#', $html, $m)) {
                return $m[0];
            }

            throw new Exception("Unexpected redirect page:\n{$url}");
        }

        throw new Exception('Redirect loop exceeded');
    }

//ToDo cUrl Header 
    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':
//ToDo cUrl Header 
                if (!$this->id_token) {
                    throw new Exception("No id_token available for FAL token request");
                }

                $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'  => '5.1.0',
                        'X-App-Name'     => 'myAudi',
                        'X-Client-Id'    => '79315a01-be62-4024-8347-a3e950e9d3ad',
                        'Accept'         => 'application/json',
                        'Accept-Encoding'=> 'gzip',
                    ],
                    'form_params' => [
                        'grant_type' => 'id_token',
                        'token'      => $this->id_token,
                        'scope'      => 'sc2:fal',
                    ],
                    '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'] ?? null;

                $this->setTokens([
                    'vwaccess_token'  => $this->vwaccess_token,
                    'vwtoken_expires' => $this->vwtoken_expires,
                    'vwrefresh_token' => $this->vwrefresh_token,
                ]);
                break;

        }

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

//ToDo cUrl Header 
    public function refreshTokens($authType)
    {
        try {
            switch ($authType) {

                case 'audietron':
                    if (!$this->refresh_token) {
                        throw new Exception('No refresh token');
                    }

                    $res = $this->client->request('POST',
                        self::API_HOST_AUDIETRON . '/login/v1/idk/token',
                        [
                            'headers' => [
                                'User-Agent'   => self::APP_USER_AGENT_SPOOF,
                                'Content-Type' => 'application/x-www-form-urlencoded',
                                'Accept'       => 'application/json',
                            ],
                            'form_params' => [
                                'grant_type'    => 'refresh_token',
                                'refresh_token' => $this->refresh_token,
                                'client_id'     => self::APP_CLIENT_ID,
                            ],
                            'http_errors' => false,
                        ]
                    );

                    if ($res->getStatusCode() !== 200) {
                        throw new Exception('Refresh failed');
                    }

                    $json = json_decode((string)$res->getBody(), true);

                    $this->access_token  = $json['access_token'];
                    $this->refresh_token = $json['refresh_token'];
                    $this->token_expires = time() + $json['expires_in'];

                    $this->setTokens([
                        'access_token'  => $this->access_token,
                        'refresh_token' => $this->refresh_token,
                        'token_expires' => $this->token_expires,
                    ]);

                    return;

                default:
                    throw new Exception('Unsupported authType');
            }

        } catch (Throwable $e) {
            // 🔥 WICHTIG: Silent Re-Auth
            if ($this->debug) {
                echo "[AUTH] Refresh failed → re-authenticate\n";
            }

            $this->authenticate($authType);
        }
    }

    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->vwaccess_token = @GetValueString(IPS_GetObjectIDByName('vwaccess_token', $this->rootId));
        $this->vwtoken_expires= (int)@GetValueString(IPS_GetObjectIDByName('vwtoken_expires', $this->rootId));

        // 🔁 Kein oder abgelaufenes Identity-Token → kompletter Login
        if (
            empty($this->access_token) ||
            empty($this->refresh_token) ||
            empty($this->token_expires) ||
            time() >= $this->token_expires
        ) {
            $this->authenticate('audietron');
            return;
        }

        // 🔁 Identity gültig, aber FAL fehlt oder abgelaufen
        if (
            empty($this->vwaccess_token) ||
            empty($this->vwtoken_expires) ||
            time() >= $this->vwtoken_expires
        ) {
            $this->ensureIdentityValid();
            $this->fetchInitialAccessTokens('audi');
            return;
        }
    }




    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 ensureIdentityValid()
    {
        if (
            empty($this->access_token) ||
            empty($this->id_token) ||
            empty($this->token_expires) ||
            time() >= $this->token_expires - 60
        ) {
            // kompletter Login → frisches id_token
            $this->authenticate('audietron');
        }
    }


    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;
    }


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

//ToDo Tabelle sortierbar machen und Spalten auswählen
    private function destinationObjectToHtmlTable($obj){
        if (
            !isset($obj->data) ||
            !isset($obj->data->destinationMemoryEntries) ||
            !is_array($obj->data->destinationMemoryEntries)
        ) {
            return '<p>Keine Daten vorhanden.</p>';
        }

        $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>UID</th>
                <th>Typ</th>
                <th>Name</th>
                <th>Straße</th>
                <th>PLZ</th>
                <th>Ort</th>
                <th>Land</th>
                <th>Latitude</th>
                <th>Longitude</th>
                <th>Erstellt</th>
                <th>Zuletzt benutzt</th>
            </tr>
        ';

        foreach ($obj->data->destinationMemoryEntries as $entry) {

            $tokens = $entry->tokens ?? new stdClass();
            $geo    = $entry->locationTokens->geoLocations[0]->location ?? null;

            $html .= '<tr>
                <td>' . htmlspecialchars($entry->uid ?? '') . '</td>
                <td>' . htmlspecialchars($entry->type ?? '') . '</td>
                <td>' . htmlspecialchars($tokens->name ?? '') . '</td>
                <td>' . htmlspecialchars($tokens->street ?? '') . ' ' . htmlspecialchars($tokens->houseNumber ?? '') . '</td>
                <td>' . htmlspecialchars($tokens->postCode ?? '') . '</td>
                <td>' . htmlspecialchars($tokens->city ?? '') . '</td>
                <td>' . htmlspecialchars($tokens->country ?? '') . '</td>
                <td>' . htmlspecialchars($geo->latitude ?? '') . '</td>
                <td>' . htmlspecialchars($geo->longitude ?? '') . '</td>
                <td>' . htmlspecialchars($entry->createTime ?? '') . '</td>
                <td>' . htmlspecialchars($entry->lastUsedTime ?? '') . '</td>
            </tr>';
        }

        $html .= '</table>';

        return $html;
    }


    private function curlInit(string $url, array $headers = [], string $method = 'GET', string $body = null)
    {
        curl_reset($this->curl);

        curl_setopt($this->curl, CURLOPT_URL, $url);
        curl_setopt($this->curl, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($this->curl, CURLOPT_FOLLOWLOCATION, true);
        curl_setopt($this->curl, CURLOPT_TIMEOUT, 30);

        if ($method !== 'GET') {
            curl_setopt($this->curl, CURLOPT_CUSTOMREQUEST, $method);
        }

        if ($body !== null) {
            curl_setopt($this->curl, CURLOPT_POSTFIELDS, $body);
        }

        if (!empty($headers)) {
            curl_setopt($this->curl, CURLOPT_HTTPHEADER, $headers);
        }
    }


    private function CreateVariableByIdent($parentId, $ident, $name, $type, $profile = ""){
        $vid = @IPS_GetObjectIDByIdent("AC_".preg_replace('/[^a-zA-Z0-9]/', '', IPS_GetName($parentId)).$ident, $parentId);
        if($vid === false) {
            $vid = IPS_CreateVariable($type);
            IPS_SetParent($vid, $parentId);
            IPS_SetName($vid, $name);
            IPS_SetIdent($vid, "AC_".preg_replace('/[^a-zA-Z0-9]/', '', IPS_GetName($parentId)).$ident);
            if($profile != "")IPS_SetVariableCustomProfile($vid, $profile);
        }
        return $vid;
    }

    private function CreateVariableByIdentsb($parentId, $ident, $name, $type, $profile = "")
    {
        $cleanIdent = preg_replace('/[^a-zA-Z0-9_]/', '', $ident);
        $fullIdent  = 'AC_' . $cleanIdent;

        $vid = @IPS_GetObjectIDByIdent($fullIdent, $parentId);
        if ($vid === false) {
            $vid = IPS_CreateVariable($type);
            IPS_SetParent($vid, $parentId);
            IPS_SetName($vid, $name);
            IPS_SetIdent($vid, $fullIdent);
            if ($profile !== "") {
                IPS_SetVariableCustomProfile($vid, $profile);
            }
        }
        return $vid;
    }
}
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');

Ich hole den Thread mal hoch, da bei mir der Wechsel auf Audi ansteht. Setzt das noch jemand ein und wenn ja, funktioniert es? Mir würde es ja reichen, wenn ich den Ladestand und die Reichweite sehen würde… :slight_smile:

Hallo Peter,
bei mir läuft es seitdem immer noch relativ problemlos. Ich habe aber nie eine Rückmeldung von jemand anderem bekommen, ob es dort ebenfalls läuft. Audi arbeitet je nach Modell mit verschiedenen API-Endpunkten, da wirst Du für Dich etwas probieren müssen welche die richtige für Dich ist. Bei meinem A3 8Y PHEV z.B. bekomme ich das meiste von einem Endpunkt, das Fahrtenbuch aber von einem anderen.

Ok, dann werde ich damit im September mal experimentieren, wenn das Auto da ist. Ich poste dann hier ein Update :slight_smile:

Habe seit einigen Wochen auch einen neuen (2025) A3 PEHV und fände es super wenn ich ihn (und unseren e-UP) in IPS einbinden könnte. Leider funktioniert der OneDrive Link (bei mir?) nicht. Bin somit etwas ratlos was ich machen soll.

Ggf kann wer Tipps geben und helfen.

Danke und Gruß Michael

Edit 1: Das Script habe ich nun entsprechend importiert. Allerdings fehlt mir nun der Vendor-Ordner.
Edit 2: Chat GPT hat geholfen… Das Script läuft und holt auch die Daten meines Audis ab. Supi!

Was war der Trick?

Hast du den e-Up drin? Meinen aktuellen ID.4 habe ich über weconnect-mqtt angebunden.

Ich habe meinen vendor-Ordner nochmal als ZIP hochgeladen und im ersten Beitrag neu verlinkt.

2 „Gefällt mir“

Moin moin,

@peter: kein Trick dabei, habe es so wie beschrieben gemacht und lief auf Anhieb. Den UP versuche ich mal am Wochenende.

@Strichcode: Danke dir. Hinsichtlich dem Vendor habe ich das mit dem Composer gemacht. GPT hat geholfen. Aber danke fürs verlinken. Bei mir sieht das Ganze auch so aus wie bei dir. Komisch finde ich die Scripte. Das ist alles doppelt da. Muss das so? Die beiden Scripte sind wie oben aufgeführt. Ggf. Darf ich nur das „Daten holen“ ausführen.

Gruß Michael

Wenn Du die String-Variablen unterhalb der Skripte meinst: Das sind nur Logs, die für jede im jeweiligen Skript aufgerufenen Funktion erstellt werden. Das kannst Du im Konfig-Abschnitt mit private $responseCache = false; deaktivieren. Vermutlich hast Du erst direkt in der Klasse mit ein paar Befehlen getestet und dann im Skript „Daten holen“.

Nee, ich meine die Scripte/Strings im Ordner „Scripte“. Die werden jedesmal bei der Ausführung neu angelegt. Und es kommen Warnungen.

Gruß Michael

Edit: habs nun verstanden und die „Logs“ ausgeschaltet. Danke Dir!!! Tolles Script.

Habe es gerade mit meinem eUP versucht. Das funktioniert leider nicht. Da der Server ja gleich ist, habe ich nur die FIN geändert. Die Daten werden angelegt, sind aber leer.

Gruß Michael

Moin,

sehr schön; bin in der Sache aber nicht involviert - falsche Mailadresse.

Gruß (anderer) Peter

Ich hatte ja schon im Eingangspost geschrieben, dass es für andere Marken/Typen andere Werte und Endpunkte gibt und die nötigen Quellen verlinkt. Das meiste zu den verschiedenen „Profilen“ findet man vermutlich hier: ioBroker.vw-connect/main.js at master · TA2k/ioBroker.vw-connect · GitHub

1 „Gefällt mir“

Hallo Strichcode,

könntest du bitte noch mal was zu den Warnungen sagen. Die kommen nämlich jedes Mal und das nervt leider ein wenig. Da ich des tiefen php nicht mächtig bin, werde ich das wohl selber nicht finden / abstellen können.

Danke und Gruß Michael

Ersetze mal bei Zeile 976 das IPS_SetIdent($vid, „AC_“.IPS_GetName($parentId).$ident); durch das hier:
IPS_SetIdent($vid, "AC_".preg_replace('/[^a-zA-Z0-9]/', '', IPS_GetName($parentId)).$ident);

Die anderen Fehlermeldungen kommen vermutlich, weil Du keine Standklimatisierung hast, diese aber abfragst. Setze in Zeile 90 ein // vor $data->getClimater = $this->getClimater();

Danke!

Das hier mag er nicht:

977 IPS_SetIdent($vid, „AC_“.preg_replace(‚/[^a-zA-Z0-9]/‘, ‚‘, IPS_GetName($parentId)).$ident);