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. ![]()
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:
-
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.
-
In der Audi.class.php die Zugangsdaten, FIN und die ID der Startkategorie für die Objekte eintragen.
-
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 Ø</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;
}
}



