- neuer Reiter "Fahrzeug": API-Schluessel eintragen und pruefen, ohne dass ein Wert je an den Browser zurueckgeht (restricted/skodaKeys.php) - ajax/skoda.php trennt Ladungen am Zustand statt an 30 Minuten Pause und rahmt sie mit der Zeile davor und danach ein - solarLog_skoda_ladepunkte.sql: Wallbox-Verlauf waehrend einer Ladung - Kachelformat "leistung_kw" fuer Quellen, die schon Kilowatt melden - Steckersymbol versteht evPlug als 1/0 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
547 lines
20 KiB
PHP
547 lines
20 KiB
PHP
<?php
|
||
/**
|
||
* Die Zugangsschlüssel der MyŠkoda-Anbindung.
|
||
*
|
||
* Eine Stelle für alles, was skoda.conf betrifft: wo die Datei liegt, wie sie
|
||
* gelesen wird, was davon die Oberfläche sehen darf und wie ein neuer
|
||
* Schlüssel hineinkommt. Vorher wusste ajax/skodaCmd.php das für sich allein;
|
||
* mit dem Reiter in den Einstellungen wären es zwei Fassungen geworden, die
|
||
* sich beim ersten Formatwechsel unterscheiden.
|
||
*
|
||
* Dieselbe Datei liest gatherSkodaData.py im SolarManager. Dass das Format
|
||
* dort ein zweites Mal steht, lässt sich nicht vermeiden - zwei Sprachen,
|
||
* zwei Repositorien. skoda.conf.example daneben ist die gemeinsame
|
||
* Beschreibung, und sie ist die Stelle, an der ein neues Feld zuerst
|
||
* auftauchen sollte.
|
||
*
|
||
* ---------------------------------------------------------------------------
|
||
* Ein Schlüssel geht nie an den Browser zurück
|
||
*
|
||
* Die Seite steht im Heimnetz ohne Anmeldung offen (isLocal() in helper.php),
|
||
* und so ein Schlüssel steuert das Fahrzeug von überall auf der Welt - Laden,
|
||
* Klimatisierung, Standort. Wer ihn abliest, braucht das Heimnetz danach nicht
|
||
* mehr. Die Maske bekommt deshalb nur zu sehen, ob ein Schlüssel hinterlegt
|
||
* ist und auf welche vier Zeichen er endet; das genügt, um zwei
|
||
* auseinanderzuhalten, und taugt zu nichts sonst.
|
||
*
|
||
* Geschrieben wird nur in eine Richtung: ein neuer Wert kommt herein, der alte
|
||
* kommt nie heraus.
|
||
* ---------------------------------------------------------------------------
|
||
*/
|
||
|
||
/** Die Datei mit den Zugangsdaten. Liegt außerhalb des Webverzeichnisses; sie
|
||
* gehört zum SolarManager, der sie ebenfalls liest.
|
||
*
|
||
* Mit define() davor umzuhängen - ein Prüflauf arbeitet dann auf einer Kopie
|
||
* und nicht auf den echten Zugangsdaten. */
|
||
defined("SKODA_KONFIG") || define("SKODA_KONFIG", "/volume1/homes/wagner/SolarManager/skoda.conf");
|
||
|
||
/** Basis der öffentlichen API. Ein GET darauf prüft einen Schlüssel. */
|
||
const SKODA_API = "https://public.api.connect.skoda-auto.cz/api/v1/vehicles/";
|
||
|
||
const SKODA_KENNUNG = "SmartController-Skoda/1.0 (+https://nas.el-wa.org/smart)";
|
||
|
||
/** Wo die Schlüssel herkommen - der Link gehört in die Maske. */
|
||
const SKODA_APP_LINK = "https://go.skoda.eu/api-keys";
|
||
|
||
/** Befehle je Stunde.
|
||
*
|
||
* Das Kontingent der API - 20 je Stunde - gilt gemessen fürs ganze Konto und
|
||
* nicht je Schlüssel (die Rechnung steht bei _LIMIT in gatherSkodaData.py).
|
||
* Abruf und Steuerung laufen in getrennten Prozessen ohne gemeinsamen
|
||
* Zähler, also ist fest aufgeteilt: 14 für den Abruf, 4 für Befehle, 2
|
||
* Reserve. Ein eigener CMD_API_KEY ändert daran nichts mehr. */
|
||
const SKODA_BEFEHLE = 4;
|
||
|
||
/** Verzeichnisse für kleine Merkzettel zwischen Weboberfläche und Manager.
|
||
* Das erste, das der Webserver beschreiben darf, gewinnt. Dieselbe Liste in
|
||
* derselben Reihenfolge steht als _ANSTOSS_ORTE in gatherSkodaData.py. */
|
||
const SKODA_ARBEITSORTE = [
|
||
"/volume1/web/smart/tiles", // falls per synoacltool freigegeben
|
||
"/var/services/tmp/smart-tiles", // sonst
|
||
];
|
||
|
||
/** Name der Anstoßdatei - siehe skodaAbrufAnstossen(). */
|
||
const SKODA_ANSTOSS_DATEI = ".skoda_abruf";
|
||
|
||
/** Name des Stundenzählers für Befehle - siehe budgetFrei() in skodaCmd.php. */
|
||
const SKODA_ZAEHLER_DATEI = ".befehle.json";
|
||
|
||
/** Abstand zweier Abrufe beim Laden und während der Fahrt. Dieselbe Zahl
|
||
* steht als _I_LADEN in gatherSkodaData.py; hier nur, um sie anzuzeigen.
|
||
* Weitere Schlüssel machen sie nicht kleiner - siehe SKODA_BEFEHLE. */
|
||
const SKODA_TAKT = 260;
|
||
|
||
/**
|
||
* Die Schlüssel, die skoda.conf kennen darf, in Anzeigereihenfolge.
|
||
*
|
||
* Als Funktion und nicht als Konstante, weil jeder Eintrag seinen Satz
|
||
* Erklärung mitbringt - die steht in der Maske und soll nicht dort noch
|
||
* einmal getippt werden.
|
||
*/
|
||
function skodaFelder()
|
||
{
|
||
return [
|
||
[
|
||
"feld" => "API_KEY",
|
||
"titel" => "Abruf",
|
||
"pflicht" => true,
|
||
"zweck" => "Holt den Fahrzeugzustand. Ohne ihn bleibt die "
|
||
. "Fahrzeugseite leer.",
|
||
],
|
||
[
|
||
"feld" => "API_KEY2",
|
||
"titel" => "Abruf, zweiter Schlüssel",
|
||
"pflicht" => false,
|
||
"zweck" => "Freiwillig, und kein dichterer Abruf: das Kontingent "
|
||
. "gilt gemessen fürs ganze Konto. Was er bringt, ist "
|
||
. "Ausfallsicherheit – läuft einer ab oder wird er "
|
||
. "widerrufen, fragt der Abruf mit dem nächsten weiter.",
|
||
],
|
||
[
|
||
"feld" => "API_KEY3",
|
||
"titel" => "Abruf, dritter Schlüssel",
|
||
"pflicht" => false,
|
||
"zweck" => "Wie der zweite. Mehr als drei liest die "
|
||
. "Konfiguration nicht.",
|
||
],
|
||
[
|
||
"feld" => "CMD_API_KEY",
|
||
"titel" => "Befehle",
|
||
"pflicht" => false,
|
||
"zweck" => "Für Laden, Klimatisierung und Lüftung. Ohne ihn "
|
||
. "schickt die Weboberfläche ihre Befehle mit dem "
|
||
. "ersten Schlüssel. Ein eigener lässt sich einzeln "
|
||
. "widerrufen, ohne den Abruf mitzunehmen.",
|
||
],
|
||
];
|
||
}
|
||
|
||
/* ----------------------------------------------------------------- Lesen -- */
|
||
|
||
/**
|
||
* skoda.conf als Zuweisungen lesen. Format: NAME=wert, eine je Zeile.
|
||
*
|
||
* Liefert null, wenn die Datei fehlt oder nicht lesbar ist - das ist der
|
||
* normale Zustand einer Installation ohne Fahrzeug und kein Fehler.
|
||
*/
|
||
function skodaKonfigLesen()
|
||
{
|
||
if (!is_readable(SKODA_KONFIG)) {
|
||
return null;
|
||
}
|
||
$zeilen = file(SKODA_KONFIG, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
||
if ($zeilen === false) {
|
||
return null;
|
||
}
|
||
$werte = [];
|
||
foreach ($zeilen as $zeile) {
|
||
$zeile = trim($zeile);
|
||
if ($zeile === "" || $zeile[0] === "#" || strpos($zeile, "=") === false) {
|
||
continue;
|
||
}
|
||
list($k, $v) = explode("=", $zeile, 2);
|
||
$werte[strtoupper(trim($k))] = trim(trim($v), "\"'");
|
||
}
|
||
return $werte;
|
||
}
|
||
|
||
/**
|
||
* Welcher Schlüssel die Befehle schickt und mit welcher VIN.
|
||
*
|
||
* Der eigene, wenn es ihn gibt - sonst der des Abrufs. Liefert null, solange
|
||
* nichts hinterlegt ist.
|
||
*/
|
||
function skodaBefehlsSchluessel()
|
||
{
|
||
$werte = skodaKonfigLesen();
|
||
if (!$werte || empty($werte["API_KEY"]) || empty($werte["VIN"])) {
|
||
return null;
|
||
}
|
||
$eigener = !empty($werte["CMD_API_KEY"]);
|
||
return [
|
||
"vin" => $werte["VIN"],
|
||
"schluessel" => $eigener ? $werte["CMD_API_KEY"] : $werte["API_KEY"],
|
||
"eigener" => $eigener,
|
||
];
|
||
}
|
||
|
||
/** Die letzten vier Zeichen, mehr darf die Maske nicht wissen. */
|
||
function skodaMaske($wert)
|
||
{
|
||
$wert = (string)$wert;
|
||
return strlen($wert) > 4 ? "…" . substr($wert, -4) : "…";
|
||
}
|
||
|
||
/**
|
||
* Wann der erste Schlüssel abläuft, laut der letzten Antwort des Fahrzeugs.
|
||
*
|
||
* gatherSkodaData.py schreibt bei jedem Abruf mit, was der Header
|
||
* X-API-Key-Expires-At sagt - bei mehreren Schlüsseln das früheste Datum.
|
||
* Genauer geht es von hier aus nicht, ohne die API zu fragen, und dafür ist
|
||
* die Auskunft zu unwichtig.
|
||
*/
|
||
function skodaAblaufAusHistorie()
|
||
{
|
||
$datum = null;
|
||
try {
|
||
$mysql = new mysqli($GLOBALS["mysql_server"], $GLOBALS["mysql_solarUser"],
|
||
$GLOBALS["mysql_solarPass"], $GLOBALS["mysql_solarDB"]);
|
||
if ($mysql->connect_errno) {
|
||
return null;
|
||
}
|
||
$res = $mysql->query("SELECT key_expires FROM skoda "
|
||
. "WHERE key_expires IS NOT NULL ORDER BY id DESC LIMIT 1;");
|
||
if ($res && ($zeile = $res->fetch_assoc())) {
|
||
$datum = $zeile["key_expires"];
|
||
}
|
||
$mysql->close();
|
||
} catch (Throwable $e) {
|
||
return null; // ohne Historie eben ohne Datum
|
||
}
|
||
return $datum;
|
||
}
|
||
|
||
/**
|
||
* Der Stand für die Maske: was hinterlegt ist, was daraus folgt.
|
||
*
|
||
* Ohne Schlüsselwerte - siehe den Kopfkommentar.
|
||
*/
|
||
function skodaStand()
|
||
{
|
||
$werte = skodaKonfigLesen();
|
||
$da = is_array($werte);
|
||
|
||
$felder = [];
|
||
$abruf = 0;
|
||
foreach (skodaFelder() as $f) {
|
||
$wert = $da && !empty($werte[$f["feld"]]) ? $werte[$f["feld"]] : "";
|
||
$f["gesetzt"] = $wert !== "";
|
||
$f["maske"] = $wert !== "" ? skodaMaske($wert) : "";
|
||
$felder[] = $f;
|
||
if ($wert !== "" && $f["feld"] !== "CMD_API_KEY") {
|
||
$abruf++;
|
||
}
|
||
}
|
||
|
||
return [
|
||
"datei" => SKODA_KONFIG,
|
||
"da" => $da,
|
||
"schreibbar" => $da && is_writable(SKODA_KONFIG),
|
||
"vin" => $da && !empty($werte["VIN"]) ? skodaMaske($werte["VIN"]) : "",
|
||
"felder" => $felder,
|
||
"abruf" => $abruf,
|
||
// Was gilt - die Zahlen, nach denen man sonst im Quelltext sucht.
|
||
"takt" => SKODA_TAKT,
|
||
"befehle" => SKODA_BEFEHLE,
|
||
"ablauf" => $da ? skodaAblaufAusHistorie() : null,
|
||
"link" => SKODA_APP_LINK,
|
||
];
|
||
}
|
||
|
||
/* ------------------------------------------------------------- Schreiben -- */
|
||
|
||
/**
|
||
* Prüft einen Schlüssel, indem einmal das Fahrzeug abgefragt wird.
|
||
*
|
||
* Lieber eine Anfrage jetzt als ein Tippfehler, der erst Minuten später als
|
||
* Zeile im Log auffällt. Sie kostet eine der 20 Anfragen, die der neue
|
||
* Schlüssel in seiner ersten Stunde hat - die er ohnehin frisch mitbringt.
|
||
*
|
||
* Zurück kommt, was die API sagt, und ihr Ablaufdatum: [ok, status, ablauf,
|
||
* text].
|
||
*/
|
||
function skodaSchluesselPruefen($schluessel, $vin)
|
||
{
|
||
$ch = curl_init(SKODA_API . rawurlencode($vin));
|
||
curl_setopt_array($ch, [
|
||
CURLOPT_RETURNTRANSFER => true,
|
||
CURLOPT_HEADER => true,
|
||
CURLOPT_USERAGENT => SKODA_KENNUNG,
|
||
CURLOPT_TIMEOUT => 10,
|
||
CURLOPT_CONNECTTIMEOUT => 5,
|
||
CURLOPT_HTTPHEADER => [
|
||
"X-API-Key: " . $schluessel,
|
||
"Accept: application/json",
|
||
],
|
||
]);
|
||
$inhalt = curl_exec($ch);
|
||
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
|
||
$kopfEnde = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
|
||
curl_close($ch);
|
||
|
||
if ($inhalt === false) {
|
||
return ["ok" => false, "status" => 0, "ablauf" => null,
|
||
"text" => "Die Škoda-API ist nicht erreichbar."];
|
||
}
|
||
|
||
$ablauf = null;
|
||
foreach (explode("\n", substr($inhalt, 0, $kopfEnde)) as $zeile) {
|
||
if (stripos($zeile, "X-API-Key-Expires-At:") === 0) {
|
||
$ablauf = trim(substr($zeile, strlen("X-API-Key-Expires-At:")));
|
||
}
|
||
}
|
||
|
||
switch ($status) {
|
||
case 200:
|
||
return ["ok" => true, "status" => 200, "ablauf" => $ablauf, "text" => ""];
|
||
case 401:
|
||
return ["ok" => false, "status" => 401, "ablauf" => null,
|
||
"text" => "Die API kennt diesen Schlüssel nicht - abgelaufen "
|
||
. "oder vertippt."];
|
||
case 403:
|
||
return ["ok" => false, "status" => 403, "ablauf" => null,
|
||
"text" => "Der Schlüssel gilt nicht für dieses Fahrzeug. In "
|
||
. "der MyŠkoda-App muss es beim Anlegen ausgewählt sein."];
|
||
case 404:
|
||
return ["ok" => false, "status" => 404, "ablauf" => null,
|
||
"text" => "Fahrzeug nicht gefunden - stimmt die VIN in "
|
||
. SKODA_KONFIG . "?"];
|
||
case 429:
|
||
// Kein Urteil über den Schlüssel: er ist in Ordnung, nur gerade
|
||
// ausgeschöpft. Das darf den Tausch nicht verhindern.
|
||
return ["ok" => true, "status" => 429, "ablauf" => null,
|
||
"text" => "Kontingent gerade erschöpft - nicht geprüft, "
|
||
. "aber übernommen."];
|
||
default:
|
||
return ["ok" => true, "status" => $status, "ablauf" => $ablauf,
|
||
"text" => "Unerwartete Antwort (HTTP " . $status . ") - nicht "
|
||
. "geprüft, aber übernommen."];
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Ein Schlüssel, wie er in einer Zeile NAME=wert stehen darf.
|
||
*
|
||
* Entscheidend ist, dass kein Zeilenumbruch hineinkommt: mit einem ließen sich
|
||
* weitere Zuweisungen unterschieben, etwa eine fremde VIN. Sonst wird bewusst
|
||
* wenig verlangt - welche Form Škoda seinen Schlüsseln morgen gibt, steht
|
||
* nicht in unserer Hand.
|
||
*/
|
||
function skodaWertPruefen($feld, $wert)
|
||
{
|
||
$wert = trim((string)$wert);
|
||
if ($wert === "") {
|
||
throw new InvalidArgumentException($feld . ": kein Wert angegeben.");
|
||
}
|
||
if (preg_match('/\s/', $wert)) {
|
||
throw new InvalidArgumentException($feld . ": ein Schlüssel enthält keine "
|
||
. "Leerzeichen oder Zeilenumbrüche - versehentlich zu viel kopiert?");
|
||
}
|
||
if (!preg_match('/^[\x21-\x7e]{8,256}$/', $wert)) {
|
||
throw new InvalidArgumentException($feld . ": das sieht nicht nach einem "
|
||
. "Schlüssel aus (8 bis 256 Zeichen, keine Sonderzeichen).");
|
||
}
|
||
return $wert;
|
||
}
|
||
|
||
/**
|
||
* skoda.conf neu schreiben.
|
||
*
|
||
* $aenderungen: Feld => neuer Wert, oder Feld => null zum Entfernen.
|
||
*
|
||
* Die Datei bleibt, wie sie ist: jede Zeile, die nicht gemeint ist, geht
|
||
* unverändert durch - Kommentare, VIN, DB_PASSWORD, Leerzeilen. Ein Feld, das
|
||
* noch nicht vorkommt, wird angehängt.
|
||
*
|
||
* Geschrieben wird daneben und dann umbenannt. Das Umbenennen ist der einzige
|
||
* Augenblick, in dem sich die Datei ändert; wer sie gerade liest - der Manager
|
||
* alle paar Minuten, skodaCmd.php bei jedem Befehl - sieht entweder den alten
|
||
* oder den neuen Stand, nie einen halben.
|
||
*/
|
||
function skodaKonfigSchreiben(array $aenderungen)
|
||
{
|
||
$roh = @file(SKODA_KONFIG);
|
||
if ($roh === false) {
|
||
throw new RuntimeException("Die Datei " . SKODA_KONFIG . " ist nicht lesbar.");
|
||
}
|
||
|
||
$offen = $aenderungen; // was noch nicht untergebracht ist
|
||
$neu = [];
|
||
foreach ($roh as $zeile) {
|
||
$blank = trim($zeile);
|
||
$name = "";
|
||
if ($blank !== "" && $blank[0] !== "#" && strpos($blank, "=") !== false) {
|
||
$name = strtoupper(trim(explode("=", $blank, 2)[0]));
|
||
}
|
||
if ($name === "" || !array_key_exists($name, $offen)) {
|
||
$neu[] = rtrim($zeile, "\r\n");
|
||
continue;
|
||
}
|
||
if ($offen[$name] !== null) {
|
||
$neu[] = $name . "=" . $offen[$name];
|
||
} // null: Zeile fällt weg
|
||
unset($offen[$name]);
|
||
}
|
||
foreach ($offen as $name => $wert) {
|
||
if ($wert !== null) {
|
||
$neu[] = $name . "=" . $wert;
|
||
}
|
||
}
|
||
|
||
// Rechte der alten Datei übernehmen: der Manager läuft unter einem anderen
|
||
// Benutzer und muss sie weiter lesen können.
|
||
$rechte = @fileperms(SKODA_KONFIG);
|
||
$rechte = $rechte ? ($rechte & 0777) : 0666;
|
||
|
||
$vorlaeufig = SKODA_KONFIG . ".neu." . getmypid();
|
||
if (@file_put_contents($vorlaeufig, implode("\n", $neu) . "\n") === false) {
|
||
throw new RuntimeException("Konnte neben " . SKODA_KONFIG . " nicht "
|
||
. "schreiben - fehlen dem Webserver die Rechte auf das Verzeichnis?");
|
||
}
|
||
@chmod($vorlaeufig, $rechte);
|
||
if (!@rename($vorlaeufig, SKODA_KONFIG)) {
|
||
@unlink($vorlaeufig);
|
||
throw new RuntimeException("Konnte " . SKODA_KONFIG . " nicht ersetzen.");
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Neue Schlüssel prüfen und speichern.
|
||
*
|
||
* $neu: Feld => Wert. Leere Werte bedeuten "unverändert" und fallen hier
|
||
* schon weg - ein leeres Feld in der Maske soll nichts löschen.
|
||
* $loeschen: Felder, die verschwinden sollen.
|
||
*
|
||
* Erst prüfen, dann schreiben: ein Schlüssel, den die API ablehnt, kommt gar
|
||
* nicht erst in die Datei. Sonst stünde der Abruf still, bis es jemand im Log
|
||
* bemerkt.
|
||
*/
|
||
function skodaSchluesselSpeichern(array $neu, array $loeschen = [])
|
||
{
|
||
$stand = skodaStand();
|
||
if (!$stand["da"]) {
|
||
throw new RuntimeException("Die Datei " . SKODA_KONFIG . " gibt es nicht. "
|
||
. "Ohne sie läuft auch der Abruf nicht - sie gehört zum SolarManager.");
|
||
}
|
||
$erlaubt = array_column(skodaFelder(), "feld");
|
||
$werte = skodaKonfigLesen();
|
||
$vin = $werte["VIN"] ?? "";
|
||
if ($vin === "") {
|
||
throw new RuntimeException("In " . SKODA_KONFIG . " steht keine VIN - "
|
||
. "ohne sie lässt sich kein Schlüssel prüfen.");
|
||
}
|
||
|
||
$aenderungen = [];
|
||
foreach ($neu as $feld => $wert) {
|
||
$feld = strtoupper((string)$feld);
|
||
if (!in_array($feld, $erlaubt, true)) {
|
||
throw new InvalidArgumentException("Unbekanntes Feld: " . $feld);
|
||
}
|
||
if (trim((string)$wert) === "") {
|
||
continue; // leer heißt: so lassen
|
||
}
|
||
$aenderungen[$feld] = skodaWertPruefen($feld, $wert);
|
||
}
|
||
|
||
/*
|
||
* Derselbe Schlüssel zweimal bringt kein zweites Kontingent - die API
|
||
* zählt den Schlüssel, nicht die Zeile. gatherSkodaData.py wirft ein
|
||
* Duplikat beim Lesen weg; hier fiele es nur als "zwei Schlüssel" auf,
|
||
* die den Abruf dann doch nicht beschleunigen.
|
||
*/
|
||
foreach ($aenderungen as $feld => $wert) {
|
||
if ($wert === null) {
|
||
continue;
|
||
}
|
||
foreach ($erlaubt as $anderes) {
|
||
if ($anderes === $feld) {
|
||
continue;
|
||
}
|
||
$schon = array_key_exists($anderes, $aenderungen)
|
||
? $aenderungen[$anderes] : ($werte[$anderes] ?? "");
|
||
if ($schon !== null && $schon !== "" && hash_equals((string)$schon, $wert)) {
|
||
throw new InvalidArgumentException($feld . ": dieser Schlüssel steht "
|
||
. "schon als " . $anderes . " da. Ein zweites Mal derselbe bringt "
|
||
. "kein zweites Kontingent - in der MyŠkoda-App einen neuen anlegen.");
|
||
}
|
||
}
|
||
}
|
||
foreach ($loeschen as $feld) {
|
||
$feld = strtoupper((string)$feld);
|
||
if (!in_array($feld, $erlaubt, true)) {
|
||
throw new InvalidArgumentException("Unbekanntes Feld: " . $feld);
|
||
}
|
||
if ($feld === "API_KEY") {
|
||
throw new InvalidArgumentException("Ohne den ersten Schlüssel gibt es "
|
||
. "keinen Abruf mehr. Er lässt sich ersetzen, aber nicht entfernen.");
|
||
}
|
||
$aenderungen[$feld] = null;
|
||
}
|
||
if (!$aenderungen) {
|
||
throw new InvalidArgumentException("Nichts einzutragen.");
|
||
}
|
||
|
||
/*
|
||
* Ab hier wird das Netz befragt, und das kann dauern. helper.php hat die
|
||
* Sitzung geöffnet und hält sie gesperrt, solange diese Anfrage läuft -
|
||
* ohne das hier stünde der ganze Browser so lange still, auch die Kacheln
|
||
* in einem anderen Reiter.
|
||
*/
|
||
if (session_status() === PHP_SESSION_ACTIVE) {
|
||
session_write_close();
|
||
}
|
||
|
||
$meldungen = [];
|
||
foreach ($aenderungen as $feld => $wert) {
|
||
if ($wert === null) {
|
||
continue;
|
||
}
|
||
$urteil = skodaSchluesselPruefen($wert, $vin);
|
||
if (!$urteil["ok"]) {
|
||
throw new InvalidArgumentException($feld . ": " . $urteil["text"]);
|
||
}
|
||
if ($urteil["text"] !== "") {
|
||
$meldungen[] = $feld . ": " . $urteil["text"];
|
||
} elseif ($urteil["ablauf"]) {
|
||
$meldungen[] = $feld . " angenommen, gültig bis "
|
||
. skodaDatumKurz($urteil["ablauf"]) . ".";
|
||
} else {
|
||
$meldungen[] = $feld . " angenommen.";
|
||
}
|
||
}
|
||
|
||
skodaKonfigSchreiben($aenderungen);
|
||
|
||
/*
|
||
* Der Manager liest skoda.conf von selbst neu, sobald sie sich geändert
|
||
* hat - aber erst, wenn der nächste Abruf ohnehin ansteht, und beim Parken
|
||
* sind das 20 Minuten. Derselbe Anstoß wie nach einem Befehl holt ihn auf
|
||
* eine halbe Minute heran; dann greift ein neuer Schlüssel sofort.
|
||
*/
|
||
skodaAbrufAnstossen();
|
||
|
||
return ["meldungen" => $meldungen, "stand" => skodaStand()];
|
||
}
|
||
|
||
/* ---------------------------------------------------------------- Beiwerk - */
|
||
|
||
/** Ein API-Datum kurz und deutsch. Unlesbares bleibt, wie es ist. */
|
||
function skodaDatumKurz($iso)
|
||
{
|
||
$zeit = strtotime((string)$iso);
|
||
return $zeit ? date("d.m.Y", $zeit) : (string)$iso;
|
||
}
|
||
|
||
/**
|
||
* Dem Abruf im solarManager Bescheid geben, dass sich gleich etwas ändert.
|
||
*
|
||
* Berührt wird eine leere Datei; gatherSkodaData.py sieht bei jedem seiner
|
||
* Durchläufe auf ihre Änderungszeit und holt den nächsten Abruf auf
|
||
* _NACH_BEFEHL Sekunden danach vor. Ein stat alle drei Sekunden ist billiger
|
||
* als eine Verbindung, die dafür offen bleiben müsste, und es geht auch dann
|
||
* nichts verloren, wenn einer der beiden Prozesse gerade neu startet.
|
||
*
|
||
* Kein Fehler, wenn es nicht klappt: der Befehl ist beim Fahrzeug, nur die
|
||
* Anzeige zieht dann später nach.
|
||
*/
|
||
function skodaAbrufAnstossen()
|
||
{
|
||
foreach (SKODA_ARBEITSORTE as $ort) {
|
||
if (is_dir($ort) && is_writable($ort)) {
|
||
@touch($ort . "/" . SKODA_ANSTOSS_DATEI);
|
||
return;
|
||
}
|
||
}
|
||
}
|