Skoda: Schluessel in den Einstellungen, Ladungen am Stueck, Wallbox in kW
- 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>
This commit is contained in:
+13
-3
@@ -14,7 +14,9 @@
|
||||
* pfad optional: Schlüssel in einer JSON-Nachricht
|
||||
* wechselrichter Liste von Namen unter solarManager/invertersN (statt topic)
|
||||
* feld bei Wechselrichtern das Feld, meist p_AC
|
||||
* format "leistung" skaliert selbst zwischen W und kW
|
||||
* format "leistung" skaliert selbst zwischen W und kW, die Quelle
|
||||
* liefert Watt; "leistung_kw" dasselbe fuer Quellen, die
|
||||
* schon in Kilowatt melden (die Wallboxen im SolarManager)
|
||||
* einheit sonst die Einheit, die hinter der Zahl steht
|
||||
* stellen Nachkommastellen (0-3)
|
||||
* negativ das Vorzeichen umdrehen - fuer Zaehler, die verkehrt
|
||||
@@ -43,6 +45,14 @@ require_once(__DIR__ . "/costs.php"); // solarDb()
|
||||
/** Höchstens drei Werte je Kachel - mehr passt nicht auf 24 x 19 Einheiten. */
|
||||
const KACHEL_MAX = 3;
|
||||
|
||||
/**
|
||||
* Die Formate, die selbst zwischen W und kW skalieren - je nachdem, in welcher
|
||||
* Einheit die Quelle meldet. Die meisten Zähler schicken Watt; die Wallboxen
|
||||
* im SolarManager (evPower, evPowerOG) schicken Kilowatt, und als Watt gelesen
|
||||
* stand bei 1 kW Ladeleistung "1 W" auf der Kachel.
|
||||
*/
|
||||
const KACHEL_LEISTUNG = ["leistung", "leistung_kw"];
|
||||
|
||||
/**
|
||||
* Vorschläge für das Symbol vor einer Zahl.
|
||||
*
|
||||
@@ -326,8 +336,8 @@ function kachelWertPruefen($wert, $nummer)
|
||||
}
|
||||
|
||||
// Entweder das Format rechnet die Einheit selbst aus, oder sie steht da.
|
||||
if (($wert["format"] ?? "") === "leistung") {
|
||||
$sauber["format"] = "leistung";
|
||||
if (in_array($wert["format"] ?? "", KACHEL_LEISTUNG, true)) {
|
||||
$sauber["format"] = $wert["format"];
|
||||
} else {
|
||||
$einheit = trim((string)($wert["einheit"] ?? ""));
|
||||
if (mb_strlen($einheit) > 8) {
|
||||
|
||||
@@ -22,7 +22,8 @@
|
||||
* restricted/kacheln.php): bis zu drei Messwerte, jeder mit
|
||||
* topic (oder wechselrichter, siehe Carport), optional pfad
|
||||
* (Schlüssel in einer JSON-Nachricht) und entweder format
|
||||
* ("leistung" skaliert selbst zwischen W und kW) oder dem Paar
|
||||
* ("leistung" skaliert selbst zwischen W und kW, "leistung_kw"
|
||||
* ebenso fuer Quellen, die schon Kilowatt melden) oder dem Paar
|
||||
* einheit und stellen (Nachkommastellen). Fehlt der Eintrag, sind
|
||||
* es die drei Thermostatwerte - Soll, Ist, Feuchte - wie bisher.
|
||||
* Eine leere Liste heisst: kein Messwert, die Kachel trägt nur
|
||||
@@ -131,7 +132,7 @@ $rooms = [
|
||||
// die man sich vor dem Carport stellt: laedt gerade jemand?
|
||||
["floor" => "AG", "mqtt" => "Carport OG", "id" => "carport_og", "x" => 72, "y" => 154,
|
||||
"werte" => [
|
||||
["topic" => "solarManager/evPowerOG", "format" => "leistung",
|
||||
["topic" => "solarManager/evPowerOG", "format" => "leistung_kw",
|
||||
"icon" => "ev-station"],
|
||||
]],
|
||||
// Veranda und Terrasse gibt es zweimal, je Wohnpartei eine. In der
|
||||
@@ -159,7 +160,7 @@ $rooms = [
|
||||
// restricted/config.php. Ohne Kuerzel im Topic ist das die EG-Wallbox.
|
||||
["floor" => "AG", "mqtt" => "Garage", "id" => "garage", "x" => 131, "y" => 135,
|
||||
"werte" => [
|
||||
["topic" => "solarManager/evPower", "format" => "leistung",
|
||||
["topic" => "solarManager/evPower", "format" => "leistung_kw",
|
||||
"icon" => "ev-station"],
|
||||
]],
|
||||
// Der Garten ist kein Bauteil wie die anderen, aber ein Ort: hier haengen
|
||||
|
||||
@@ -22,6 +22,9 @@
|
||||
require_once(__DIR__ . "/costs.php");
|
||||
require_once(__DIR__ . "/kacheln.php");
|
||||
require_once(__DIR__ . "/reiter.php");
|
||||
// Nur wegen SKODA_APP_LINK - der Weg zu den Schlüsseln gehört neben das Feld,
|
||||
// in das sie eingetragen werden, und der Link soll nur an einer Stelle stehen.
|
||||
require_once(__DIR__ . "/skodaKeys.php");
|
||||
|
||||
/*
|
||||
* Die Reiter. Reihenfolge ist Anzeigereihenfolge; der erste ist der offene,
|
||||
@@ -31,6 +34,7 @@ $reiter = [
|
||||
["id" => "preise", "titel" => "Preise", "symbol" => "bi-currency-euro"],
|
||||
["id" => "kacheln", "titel" => "Home-Kacheln", "symbol" => "bi-grid-1x2"],
|
||||
["id" => "geraete", "titel" => "Geräte", "symbol" => "bi-house-gear"],
|
||||
["id" => "fahrzeug", "titel" => "Fahrzeug", "symbol" => "bi-ev-front"],
|
||||
];
|
||||
// "merken": der offene Reiter steht in der Adresse und uebersteht ein
|
||||
// Neuladen - siehe reiterMerken() in js/solar/common.js.
|
||||
@@ -192,6 +196,51 @@ try {
|
||||
</div>
|
||||
</div>
|
||||
<?= reiterFlaecheZu() ?><!-- /Geräte -->
|
||||
|
||||
<!-- =================================================== Fahrzeug ==== -->
|
||||
<!-- Die Zugangsschlüssel der MyŠkoda-Anbindung. Sie laufen ab und
|
||||
müssen dann getauscht werden - vorher hieß das: per SSH auf die NAS
|
||||
und skoda.conf im Editor öffnen.
|
||||
|
||||
Die Zeilen zeichnet js/solar/settings.js aus dem, was
|
||||
ajax/settings.php?action=skoda-keys liefert. Was dort NICHT
|
||||
mitkommt, sind die Schlüssel selbst: die Seite steht im Heimnetz
|
||||
ohne Anmeldung offen, und ein solcher Schlüssel steuert das
|
||||
Fahrzeug von überall auf der Welt. Zu sehen ist nur, ob einer
|
||||
hinterlegt ist und auf welche vier Zeichen er endet. -->
|
||||
<?= reiterFlaecheAuf("fahrzeug", $reiterOpt) ?>
|
||||
<div class="row">
|
||||
<div class="col-12 col-xxl-8">
|
||||
<div class="card mb-4" id="skodaKarte">
|
||||
<div class="card-header pb-2 pt-2">
|
||||
<h3 class="card-title"><i class="bi bi-key me-2"></i>Zugang zum Fahrzeug</h3>
|
||||
<div class="card-tools">
|
||||
<button type="button" class="btn btn-tool" id="skodaNeuLaden" title="Neu laden">
|
||||
<i class="bi bi-arrow-counterclockwise"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="text-secondary small mb-3">
|
||||
Die Schlüssel werden in der MyŠkoda-App erzeugt, gelten nur für die
|
||||
dort ausgewählten Fahrzeuge und laufen nach einiger Zeit ab. Ein neuer
|
||||
wird hier eingetragen und sofort einmal ausprobiert – was die API
|
||||
ablehnt, landet gar nicht erst in der Konfiguration.
|
||||
</p>
|
||||
<p class="mb-3">
|
||||
<a class="btn btn-sm btn-outline-primary"
|
||||
href="<?= SKODA_APP_LINK ?>" target="_blank" rel="noopener">
|
||||
<i class="bi bi-box-arrow-up-right me-1"></i>Schlüssel in der MyŠkoda-App verwalten
|
||||
</a>
|
||||
</p>
|
||||
<div id="skodaListe">
|
||||
<p class="text-secondary small mb-0">Wird geladen …</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?= reiterFlaecheZu() ?><!-- /Fahrzeug -->
|
||||
</div><!-- /tab-content -->
|
||||
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,546 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user