BMW connected drive in IPS?

Hallo,

ich habe endlich erste Erfolge in der Portierung, ich konnte ein Token holen und refreshen.

Ich bitte darum, das jeder, der mag, dieses Script mal ablaufen lässt, natürlich mit eigenem user und password

<?php

declare(strict_types=1);

$user = 'xxx@tdl';
$password = 'zzzz';

$with_debug = false;

function urlsafe_b64encode($string)
{
    $data = base64_encode($string);
    $data = str_replace(['+', '/'], ['-', '_'], $data);
    $data = rtrim($data, '=');
    return $data;
}

$region = 'RestOfWorld';

$oauth_config_url = '/eadrax-ucs/v1/presentation/oauth/config';

$server_urls_eadrax = [
    'NorthAmerica' => 'cocoapi.bmwgroup.us',
    'RestOfWorld'  => 'cocoapi.bmwgroup.com',
];

$ocp_apim_key = [
    'NorthAmerica' => '31e102f5-6f7e-7ef3-9044-ddce63891362',
    'RestOfWorld'  => '4f1c85a3-758f-a37d-bbb6-f8704494acfa',
];

$baseurl = 'https://' . $server_urls_eadrax[$region];
$x_user_agent_pre = 'android(v1.07_20200330);';
$x_user_agent_post = ';1.7.0(11152)';

$ch = curl_init();
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_2TLS);
curl_setopt($ch, CURLOPT_MAXREDIRS, 50);
curl_setopt($ch, CURLOPT_HTTP09_ALLOWED, true);
curl_setopt($ch, CURLOPT_TCP_KEEPALIVE, true);
curl_setopt($ch, CURLOPT_COOKIEFILE, '');
curl_setopt($ch, CURLOPT_HEADER, true);
curl_setopt($ch, CURLINFO_HEADER_OUT, true);

echo '*** get config' . PHP_EOL;

$config_url = $baseurl . '/' . $oauth_config_url;
$header = [
    'ocp-apim-subscription-key: ' . $ocp_apim_key[$region],
    'x-user-agent: ' . $x_user_agent_pre . 'bmw' . $x_user_agent_post,
];

echo 'config_url=' . $config_url . PHP_EOL;
if ($with_debug) {
    echo 'header=' . print_r($header, true) . PHP_EOL;
}

curl_setopt($ch, CURLOPT_URL, $config_url);
curl_setopt($ch, CURLOPT_HTTPHEADER, $header);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');

$response = curl_exec($ch);
$cerrno = curl_errno($ch);
$cerror = $cerrno ? curl_error($ch) : '';
$httpcode = curl_getinfo($ch, CURLINFO_HTTP_CODE);

$curl_info = curl_getinfo($ch);
$header_size = $curl_info['header_size'];
$head = substr($response, 0, $header_size);
$body = substr($response, $header_size);

echo 'errno=' . $cerrno . '(' . $cerror . '), httpcode=' . $httpcode . PHP_EOL;
echo 'body=' . $body . PHP_EOL;

$oauth_settings = json_decode($body, true);
if ($with_debug) {
    echo 'oauth_settings=' . print_r($oauth_settings, true) . PHP_EOL;
}
echo PHP_EOL;

echo '*** authenticate, step 1' . PHP_EOL;

# Setting up PKCS data
$verifier_bytes = random_bytes(64);
$code_verifier = urlsafe_b64encode($verifier_bytes);
//echo 'code_verifier='.$code_verifier.PHP_EOL;

$challenge_bytes = hash('sha256', $code_verifier, true);
$code_challenge = urlsafe_b64encode($challenge_bytes);
//echo 'code_challenge='.$code_challenge.PHP_EOL;

$state_bytes = random_bytes(16);
$state = urlsafe_b64encode($state_bytes);
//echo 'state='.$state.PHP_EOL;

$oauth_authenticate_url = '/gcdm/oauth/authenticate';

$gcdm_base_url = $oauth_settings['gcdmBaseUrl'];
$auth_url = $gcdm_base_url . $oauth_authenticate_url;

$header = [
    'Content-Type: application/x-www-form-urlencoded',
];

$oauth_base_values = [
    'client_id'             => $oauth_settings['clientId'],
    'response_type'         => 'code',
    'redirect_uri'          => $oauth_settings['returnUrl'],
    'state'                 => $state,
    'nonce'                 => 'login_nonce',
    'scope'                 => implode(' ', $oauth_settings['scopes']),
    'code_challenge'        => $code_challenge,
    'code_challenge_method' => 'S256',
];

if ($with_debug) {
    echo 'oauth_base_values=' . print_r($oauth_base_values, true) . PHP_EOL;
}

$postfields = $oauth_base_values;
$postfields['grant_type'] = 'authorization_code';
$postfields['username'] = $user;
$postfields['password'] = $password;

echo 'auth_url=' . $auth_url . PHP_EOL;
if ($with_debug) {
    echo 'header=' . print_r($header, true) . PHP_EOL;
    echo 'postfields=' . print_r($postfields, true) . PHP_EOL;
}

curl_setopt($ch, CURLOPT_URL, $auth_url);
curl_setopt($ch, CURLOPT_HTTPHEADER, $header);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($postfields));

$response = curl_exec($ch);
$cerrno = curl_errno($ch);
$cerror = $cerrno ? curl_error($ch) : '';
$httpcode = curl_getinfo($ch, CURLINFO_HTTP_CODE);

$curl_info = curl_getinfo($ch);
$header_size = $curl_info['header_size'];
$head = substr($response, 0, $header_size);
$body = substr($response, $header_size);
preg_match_all('|Set-Cookie: (.*);|U', $head, $results);
$cookies = explode(';', implode(';', $results[1]));

echo 'errno=' . $cerrno . '(' . $cerror . '), httpcode=' . $httpcode . PHP_EOL;
if ($with_debug) {
    echo 'body=' . $body . PHP_EOL;
    echo 'cookies=' . print_r($cookies, true) . PHP_EOL;
}

$jbody = json_decode($body, true);
if ($with_debug) {
    echo 'jbody=' . print_r($jbody, true) . PHP_EOL;
}
$redirect_uri = substr($jbody['redirect_to'], strlen('redirect_uri='));
$redirect_parts = parse_url($redirect_uri);
if ($with_debug) {
    echo 'redirect_parts=' . print_r($redirect_parts, true) . PHP_EOL;
}

if ($redirect_parts == false || isset($redirect_parts['query']) == false) {
    echo 'missing element "query" in "' . $redirect_uri . '"' . PHP_EOL;
    return false;
}

parse_str($redirect_parts['query'], $redirect_opts);
if ($with_debug) {
    echo 'redirect_opts=' . print_r($redirect_opts, true) . PHP_EOL;
}

foreach (['authorization'] as $key) {
    if (isset($redirect_opts[$key]) == false) {
        echo 'missing element "' . $key . '" in "' . $redirect_parts['query'] . '"' . PHP_EOL;
        return false;
    }
}

echo 'authorization="' . $redirect_opts['authorization'] . '"' . PHP_EOL;
echo PHP_EOL;

echo '*** authenticate, step 2' . PHP_EOL;

$postfields = $oauth_base_values;
$postfields['authorization'] = $redirect_opts['authorization'];

$header = [
    'Content-Type: application/x-www-form-urlencoded',
];
foreach ($cookies as $cookie) {
    $header[] = 'Cookie: ' . $cookie;
}

echo 'auth_url=' . $auth_url . PHP_EOL;
if ($with_debug) {
    echo 'header=' . print_r($header, true) . PHP_EOL;
    echo 'postfields=' . print_r($postfields, true) . PHP_EOL;
}

curl_setopt($ch, CURLOPT_URL, $auth_url);
curl_setopt($ch, CURLOPT_HTTPHEADER, $header);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($postfields));

$response = curl_exec($ch);
$cerrno = curl_errno($ch);
$cerror = $cerrno ? curl_error($ch) : '';
$httpcode = curl_getinfo($ch, CURLINFO_HTTP_CODE);

$curl_info = curl_getinfo($ch);
$header_size = $curl_info['header_size'];
$head = substr($response, 0, $header_size);
$body = substr($response, $header_size);

echo 'errno=' . $cerrno . '(' . $cerror . '), httpcode=' . $httpcode . PHP_EOL;
if ($with_debug) {
    echo 'head=' . $head . PHP_EOL;
    echo 'body=' . $body . PHP_EOL;
}

preg_match_all('|location: (.*)|', $head, $results);
if ($with_debug) {
    echo 'results=' . print_r($results, true) . PHP_EOL;
}

$location = $results[1][0];
$location_parts = parse_url($location);
if ($with_debug) {
    echo 'location_parts=' . print_r($location_parts, true) . PHP_EOL;
}

if ($location_parts == false || isset($location_parts['query']) == false) {
    echo 'missing element "query" in "' . $location . '"' . PHP_EOL;
    return false;
}

parse_str($location_parts['query'], $location_opts);
if ($with_debug) {
    echo 'location_opts=' . print_r($location_opts, true) . PHP_EOL;
}

foreach (['code'] as $key) {
    if (isset($location_opts[$key]) == false) {
        echo 'missing element "' . $key . '" in "' . $location_opts['query'] . '"' . PHP_EOL;
        return false;
    }
}

echo PHP_EOL;

echo '*** get token' . PHP_EOL;

$oauth_authorization = base64_encode($oauth_settings['clientId'] . ':' . $oauth_settings['clientSecret']);

$token_url = $oauth_settings['tokenEndpoint'];

$header[] = 'Authorization: Basic ' . $oauth_authorization;

$postfields = [
    'code'             => $location_opts['code'],
    'code_verifier'    => $code_verifier,
    'redirect_uri'     => $oauth_settings['returnUrl'],
    'grant_type'       => 'authorization_code',
];

echo 'token_url=' . $token_url . PHP_EOL;
if ($with_debug) {
    echo 'header=' . print_r($header, true) . PHP_EOL;
    echo 'postfields=' . print_r($postfields, true) . PHP_EOL;
}

curl_setopt($ch, CURLOPT_URL, $token_url);
curl_setopt($ch, CURLOPT_HTTPHEADER, $header);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($postfields));

$response = curl_exec($ch);
$cerrno = curl_errno($ch);
$cerror = $cerrno ? curl_error($ch) : '';
$httpcode = curl_getinfo($ch, CURLINFO_HTTP_CODE);

$curl_info = curl_getinfo($ch);
$header_size = $curl_info['header_size'];
$head = substr($response, 0, $header_size);
$body = substr($response, $header_size);

echo 'errno=' . $cerrno . '(' . $cerror . '), httpcode=' . $httpcode . PHP_EOL;
if ($with_debug) {
    echo 'body=' . $body . PHP_EOL;
}

$jbody = json_decode($body, true);
if ($with_debug) {
    echo 'jbody=' . print_r($jbody, true) . PHP_EOL;
}

foreach (['access_token', 'refresh_token', 'expires_in'] as $key) {
    if (isset($jbody[$key]) == false) {
        echo 'missing element "query" in "' . $body . '"' . PHP_EOL;
        return false;
    }
}

$access_token = $jbody['access_token'];
$refresh_token = $jbody['refresh_token'];
$expires_in = $jbody['expires_in'];

echo 'access_token=' . $access_token . ', refresh_token=' . $refresh_token . ', expires_in=' . $expires_in . PHP_EOL;

echo PHP_EOL;

$header = [
    'Content-Type: application/x-www-form-urlencoded',
    'Authorization: Basic ' . $oauth_authorization,
];

$postfields = [
    'grant_type'    => 'refresh_token',
    'refresh_token' => $refresh_token,
];

$token_url = $oauth_settings['tokenEndpoint'];

echo '*** refresh token' . PHP_EOL;

echo 'token_url=' . $token_url . PHP_EOL;
if ($with_debug) {
    echo 'header=' . print_r($header, true) . PHP_EOL;
    echo 'postfields=' . print_r($postfields, true) . PHP_EOL;
}

curl_setopt($ch, CURLOPT_URL, $token_url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
curl_setopt($ch, CURLOPT_HTTPHEADER, $header);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_2TLS);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($postfields));
curl_setopt($ch, CURLOPT_HEADER, true);

$response = curl_exec($ch);
$cerrno = curl_errno($ch);
$cerror = $cerrno ? curl_error($ch) : '';
$httpcode = curl_getinfo($ch, CURLINFO_HTTP_CODE);

$curl_info = curl_getinfo($ch);
$header_size = $curl_info['header_size'];
$head = substr($response, 0, $header_size);
$body = substr($response, $header_size);

echo 'errno=' . $cerrno . '(' . $cerror . '), httpcode=' . $httpcode . PHP_EOL;
if ($with_debug) {
    echo 'body=' . $body . PHP_EOL;
}

$jbody = json_decode($body, true);
if ($with_debug) {
    echo 'jbody=' . print_r($jbody, true) . PHP_EOL;
}

$access_token = $jbody['access_token'];
$refresh_token = $jbody['refresh_token'];
$expires_in = $jbody['expires_in'];

foreach (['access_token', 'refresh_token', 'expires_in'] as $key) {
    if (isset($jbody[$key]) == false) {
        echo 'missing element "query" in "' . $body . '"' . PHP_EOL;
        return false;
    }
}

echo 'access_token=' . $access_token . ', refresh_token=' . $refresh_token . ', expires_in=' . $expires_in . PHP_EOL;

Die Antwort sollte etwa wie folgt sein

*** get config
config_url=https://cocoapi.bmwgroup.com//eadrax-ucs/v1/presentation/oauth/config
errno=0(), httpcode=200
body={"clientName":"mybmwapp","clientSecret":"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx","clientId":"yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy","gcdmBaseUrl":"https://customer.bmwgroup.com","returnUrl":"com.bmw.connected://oauth","brand":"bmw","language":"en","country":"US","authorizationEndpoint":"https://customer.bmwgroup.com/oneid/login","tokenEndpoint":"https://customer.bmwgroup.com/gcdm/oauth/token","scopes":["openid","profile","email","offline_access","smacc","vehicle_data","perseus","dlm","svds","cesim","vsapi","remote_services","fupo","authenticate_user"],"promptValues":["login"]}

*** authenticate, step 1
auth_url=https://customer.bmwgroup.com/gcdm/oauth/authenticate
errno=0(), httpcode=200
authorization="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"

*** authenticate, step 2
auth_url=https://customer.bmwgroup.com/gcdm/oauth/authenticate
errno=0(), httpcode=302

*** get token
token_url=https://customer.bmwgroup.com/gcdm/oauth/token
errno=0(), httpcode=200
access_token=aaaaaaaaaaaaaaaaaaaaaaaaaaa, refresh_token=rrrrrrrrrrrrrrrrrrrrrrrrrrr, expires_in=3599

*** refresh token
token_url=https://customer.bmwgroup.com/gcdm/oauth/token
errno=0(), httpcode=200
access_token=aaaaaaaaaaaaaaaaaaaaaaaaaaa, refresh_token=rrrrrrrrrrrrrrrrrrrrrrrrrrr, expires_in=3599

würde mich über Antworten freuen

demel

Hallo,

ja funktioniert bei mir ebenso.
Danke für Deine Bemühungen.

Gruß

Hallo demel,

funktioniert, die Antwort kommt entsprechend. Danke! Müssen an dem Modul vom Anwender nun Änderungen vorgenommen werden oder wird das noch ins Modul implementiert?

VG

das ist nur der Test, ob das Login funktioniert. Geht halt mit einem Script einfacher …
bisher habe ich 3 positive Rückmeldungen, eine davon aus CH ( früher gab es für jedes Land eigene Zugangs-Server …)

Als nächstes muss ich das in das Modul übertragen und dann alle API-Calls anpassen/überprüfen.
Das wird noch etwas dauern, aber die Login-Prozedur ist immer das komplizierteste.

Wird also noch etwas dauern, aber ich bin vorsichtig optimistisch. Interessant ist wirklich die Frage, welche Daten es insgesamt (noch) gibt.

demel

Sieht sowohl beim meinem BMW als auch bei Mini gut aus.

Sieht auch bei mir gut aus.

nur ein kleiner Zwischenstand:

ich habe das wieder so weit hinbekommen, das das Login klappt und der Abruf der wichtigsten Daten sowie die bisherigen Steuerfunktionen.
Es fehlen auf jeden Fall bei „elektrisch“: Brutto-Batteriekapazität, Netto-Batteriekapazität, die Daten gibt es nicht mehr und ich bin bei dem erwarteten Ende des Ladens noch „am basteln“.

ich hoffe sehr, das BMW das nicht direkt wieder ändert - es wurden alle API-Calls nun ersetzt und zusammengefasst.

Eine Frage an die Anwender: es gibt ein paar Variablen, die ich umbenennen wollte, andere Variablentyp und -profile - gibt es irgendjemand, der auf bestimmte Variablen irgendwelche Verknüpfungen/Automatismen hat?

demel

3 „Gefällt mir“

Bei mir dient es rein zur Information, also keine Verknüpfungen oder Automatismen.

Gruß
Burkhard

Ja. Ich verwende den SoC (also den prozentualen Akkustand) für meine Ladesteuerungen (z.B. Ladestop bei X %).
Ansonsten zeige ich den SoC, die Restreichweite sowie verbleibende Ladezeit an.

So, nun habe ich die erste Version im ModulStore als Beta eingestellt.

Es fallen ein paar Variablen raus, ich habe jetzt erstmal alles reingepackt, was ich hingekriegt habe.

für die E-Autos: das geplante Ende der Ladezeit ist noch offen, da es etwas Trick ist. Ein Feld gibt es dafür nicht, aber es gibt ein Textfeld („Lade-Information“), und dieses Test muss ich Parsen.

Dazu müsste ich aber wissen, was so drin stehe könnten.

Bitte also diese Variable archivieren und dir die auftretenden Variationen zukommen lassen

danke
demel

Hallo demel,

danke für Dein Einsatz.
Leider fehlt ein Komma in der Modul.json. Ich habe Die ein PR auf Github gemacht.

Attain

Auf meinem Testsystem läufts schon mal:

Danke, das kommt von Änderungen in letzter Sekunde.
Hatte nur in dem Modul.json „noch eben“ die url eingetragen …

PR habe ich übernommen. leider habe ich im Augenblock Probleme das Modul wieder zu veröffentlichen
ich muss mal Dr.Niels anschreiben.

demel

Es hat noch die UUID vom Original Modul von Fonzo. Und das gibt es noch im Store.
Ich könnte mir vorstellen das das nicht geht.

PR Nr2 auf Github

Attain

Das Modul von Fonzo ist auf mich übertragen worden, genau damit Name usw gleich bleibt.
Das sollte schon funktionieren. Hat ja auch, aber nun ist die Entwickler-Modul-Oberfläche in einem komische Zustand.

Das mit dem 2. PR ist natürlich auch gut, diese Variation hatte noch ich nicht.

demel

Nabend und danke!

Ich bekomme bei drei Verbrennern diese Meldung, wenn ich im Formular Änderungen machen möchte. Habe bereits neue Instanzen angelegt, Fehler bleibt:

Fatal error: Uncaught TypeError: json_decode() expects parameter 1 to be string, bool given in /var/lib/symcon/modules/IPSymconBMWConnectedDrive/BMW/module.php:2068
Stack trace:
#0 /var/lib/symcon/modules/IPSymconBMWConnectedDrive/BMW/module.php(2068): json_decode(false, true)
#1 /var/lib/symcon/modules/IPSymconBMWConnectedDrive/BMW/module.php(441): BMWConnectedDrive->UpdateRemoteServiceStatus()
#2 /-(3): BMWConnectedDrive->ApplyChanges()
#3 {main}
thrown in /var/lib/symcon/modules/IPSymconBMWConnectedDrive/BMW/module.php on line 2068
Abort Processing during Fatal-Error: Uncaught TypeError: json_decode() expects parameter 1 to be string, bool given in /var/lib/symcon/modules/IPSymconBMWConnectedDrive/BMW/module.php:2068
Stack trace:
#0 /var/lib/symcon/modules/IPSymconBMWConnectedDrive/BMW/module.php(2068): json_decode(false, true)
#1 /var/lib/symcon/modules/IPSymconBMWConnectedDrive/BMW/module.php(441): BMWConnectedDrive->UpdateRemoteServiceStatus()
#2 /-(3): BMWConnectedDrive->ApplyChanges()
#3 {main}
thrown
Error in Script /var/lib/symcon/modules/IPSymconBMWConnectedDrive/BMW/module.php on Line 2068 (Code: -32603)

Beim testweise deaktivieren der Instanz kommt die Meldung "Fehler beim Übernehmen der Änderungen

Warning: Timer UpdateRemoteserviceHistory existiert nicht in /var/lib/symcon/modules/IPSymconBMWConnectedDrive/BMW/module.php on line 426
(Code: -32603)"

Vielleicht hat es damit etwas zu tun.

Update: Die Api scheint mich momentan nicht zu mögen: Error 502.
Ich teste morgen mal weiter.

Also in Zeile 426 muss es heißen:

UpdateRemoteServiceStatus

zu Zeile 2068: interessantes Problem, bedeutet, das es keine RemoteService-Status zu einem bestimmten Event gibt.
hatte ich nicht abgefangen, aber komisch ist es schon … grübel … ok, ich habe einen Fix, ist aber im nur im GitHub (wie bisher auf Branch oauth_api)
Hintergrund: es war irgend eine remote-service noch auch Status „PENDING“ ?!? und das Abfragen des Status hat nicht wirklich funktioniert

zu dem 2. Fehler: ist auch gefixed (im GitHub)

jepp, gerade gefixe (in GitHub)

Danke, funktioniert!

Den Verschlussstatus scheint es nicht mehr zu geben. Also Gesamtstatus: alles zu
Vielleicht bin ich auch einfach Nachtblind :slight_smile:

Und das Bild fehlt jetzt leider :frowning: