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:
2026-09-15 09:30:18 +02:00
co-authored by Claude Opus 5
parent b01e938905
commit deb81045d5
13 changed files with 1272 additions and 126 deletions
+24
View File
@@ -10,6 +10,9 @@
* GET ?action=geraete Geraete mit Raum, Verwendung und Alter
* POST ?action=geraete-raeume {"rooms": {"12": "OG/Bad", ...}}
* POST ?action=geraet-loeschen {"id": 12}
* GET ?action=skoda-keys Stand der Fahrzeug-Schluessel (ohne Werte!)
* POST ?action=skoda-keys-save {"felder": {"API_KEY2": "..."},
* "loeschen": ["API_KEY3"]}
*
* Ein reiner Verteiler: geprueft und geschrieben wird in
* restricted/costs.php bzw. restricted/kacheln.php, damit Maske und Endpunkt
@@ -20,6 +23,7 @@ require_once("../helper.php");
require_once("../restricted/costs.php");
require_once("../restricted/kacheln.php");
require_once("../restricted/automations.php");
require_once("../restricted/skodaKeys.php");
if (!checkLogin()) {
http_response_code(403);
@@ -86,11 +90,23 @@ if ($_SERVER["REQUEST_METHOD"] === "POST") {
$name = geraetLoeschen($body["id"] ?? 0);
jsonAntwort(["geloescht" => $name, "geraete" => geraeteListe()]);
case "skoda-keys-save":
/*
* Neue Schluessel eintragen. Zurueck kommt der neue Stand -
* aber nie ein Wert, siehe restricted/skodaKeys.php.
*/
jsonAntwort(skodaSchluesselSpeichern(
$body["felder"] ?? [], $body["loeschen"] ?? []));
default:
jsonAntwort(["error" => "Unbekannte Aktion."], 400);
}
} catch (InvalidArgumentException $e) {
jsonAntwort(["error" => $e->getMessage()], 400);
} catch (RuntimeException $e) {
// Nicht die Eingabe, sondern die Umgebung - Datei fehlt, Rechte
// fehlen. Die Meldung sagt schon, was zu tun ist; kein Vorspann.
jsonAntwort(["error" => $e->getMessage()], 500);
} catch (Throwable $e) {
jsonAntwort(["error" => "Speichern fehlgeschlagen: " . $e->getMessage()], 500);
}
@@ -114,6 +130,14 @@ if ($action === "kacheln") {
}
}
if ($action === "skoda-keys") {
try {
jsonAntwort(skodaStand());
} catch (Throwable $e) {
jsonAntwort(["error" => "Schlüssel nicht abrufbar: " . $e->getMessage()], 500);
}
}
if ($action === "geraete") {
try {
jsonAntwort(["geraete" => geraeteListe(), "raeume" => raumListe()]);
+232 -35
View File
@@ -42,54 +42,100 @@ if ($mysql->connect_errno) {
$mysql->set_charset("utf8mb4");
/*
* Ladevorgänge aus den Einzelzeilen schneiden. Aufeinanderfolgende Zeilen mit
* chg_state = 'CHARGING' gehören zusammen, eine Pause über 30 Minuten trennt
* zwei Vorgänge. Wird von mehreren Abschnitten gebraucht, deshalb einmal hier.
* Ladevorgänge aus den Einzelzeilen schneiden. Wird von mehreren Abschnitten
* gebraucht, deshalb einmal hier.
*
* Eine Ladung ist eine ununterbrochene Folge von Zeilen mit chg_state =
* 'CHARGING'. Getrennt wird durch eine Zeile mit einem anderen Zustand, nicht
* durch die Zeit dazwischen: gatherSkodaData.py schreibt nur, wenn das
* Fahrzeug etwas Neues meldet, und beim Laden an der Wallbox tut es das
* mitunter nur einmal in der Stunde. Früher trennte eine Pause über 30
* Minuten - eine Ladung von 51 auf 80 % am 13.09.2026 zerfiel dadurch in drei
* Stücke mit 0, 4 und 0 Prozentpunkten. Hört das Fahrzeug auf zu laden,
* steht das in der nächsten Zeile; die Zeit allein trennt nur noch, wenn sie
* so lang ist, dass der Abruf offenbar nicht lief (LADUNG_LUECKE_S).
*
* Dazu gehören die beiden Zeilen, die die Ladung einrahmen. Die davor sagt,
* mit welchem Ladestand und Zählerstand sie begann - die erste CHARGING-Zeile
* kommt oft erst Minuten später und hat dann schon geladen. Die danach
* (CONNECT_CABLE, READY_FOR_CHARGING, ...) trägt den erreichten Ladestand:
* bei einer Ladung bis zum Ziel ist das die einzige Zeile mit 80 %.
*
* Die Energie kommt aus der Differenz der Wallbox-Zählerstände statt aus einer
* Summe über Leistungswerte - das trifft den Ladevorgang auf die Wattstunde.
* Steht der Zähler still, wurde auswärts geladen (Schnellladesäule); dann
* bleibt wh null und die Kapazitätsrechnung lässt die Ladung aus.
* Ein Zählerstand 0 ist kein Stand, sondern ein ausgefallener Abruf der
* Wallbox, und bleibt außen vor. Steht der Zähler still, wurde auswärts
* geladen (Schnellladesäule); dann bleibt wh null und die Kapazitätsrechnung
* lässt die Ladung aus.
*/
const LADUNG_LUECKE_S = 3 * 3600;
function sitzungenSQL($seit)
{
$luecke = LADUNG_LUECKE_S;
return "
SELECT MIN(datetime) AS beginn, MAX(datetime) AS ende,
MIN(soc) AS soc_von, MAX(soc) AS soc_bis,
CAST(MAX(soc) AS SIGNED) - CAST(MIN(soc) AS SIGNED) AS zuwachs,
MIN(soc_start) AS soc_von, MAX(soc) AS soc_bis,
CAST(MAX(soc) AS SIGNED) - CAST(MIN(soc_start) AS SIGNED) AS zuwachs,
MAX(chg_type) AS art, MAX(target_soc) AS ziel,
ROUND(AVG(chg_kw), 2) AS kw_mittel, ROUND(MAX(chg_kw), 2) AS kw_spitze,
MAX(wb_wh_total) - MIN(wb_wh_total) AS wh,
ROUND(SUM(kwh_teil), 3) AS kwh_fahrzeug,
ROUND(AVG(pv_kw), 2) AS pv_kw, COUNT(*) AS punkte
ROUND(AVG(CASE WHEN laedt THEN chg_kw END), 2) AS kw_mittel,
ROUND(MAX(CASE WHEN laedt THEN chg_kw END), 2) AS kw_spitze,
COALESCE(MAX(NULLIF(wb_wh_total, 0)) - MIN(wh_start), 0) AS wh,
/*
* Fahrzeugseitige Energie nur, wenn sie lückenlos integriert werden
* konnte - siehe kwh_teil. Sonst NULL: eine halbe Summe sähe aus
* wie eine kleine Ladung und verfälschte die Kapazitätsrechnung.
*/
CASE WHEN SUM(zu_lang) = 0 THEN ROUND(SUM(kwh_teil), 3) END AS kwh_fahrzeug,
ROUND(AVG(CASE WHEN laedt THEN pv_kw END), 2) AS pv_kw,
COUNT(*) AS punkte
FROM (
SELECT s.*, SUM(s.neu) OVER (ORDER BY s.datetime) AS sitzung FROM (
SELECT datetime, soc, chg_state, chg_type, chg_kw, target_soc,
wb_wh_total, pv_kw,
CASE WHEN LAG(chg_state) OVER w <=> 'CHARGING'
AND TIMESTAMPDIFF(MINUTE, LAG(datetime) OVER w, datetime) <= 30
THEN 0 ELSE 1 END AS neu,
SELECT b.*, SUM(b.neu) OVER (ORDER BY b.datetime) AS sitzung FROM (
SELECT a.*,
a.chg_state = 'CHARGING' AS laedt,
(a.st_vor <=> 'CHARGING' AND a.dt_vor <= $luecke) AS weiter,
CASE WHEN a.chg_state = 'CHARGING'
AND NOT (a.st_vor <=> 'CHARGING' AND a.dt_vor <= $luecke)
THEN 1 ELSE 0 END AS neu,
-- Beginn aus der Zeile davor, wenn sie nah genug liegt.
CASE WHEN a.chg_state = 'CHARGING' AND NOT (a.st_vor <=> 'CHARGING')
AND a.dt_vor <= $luecke
THEN LEAST(a.soc, COALESCE(a.soc_vor, a.soc))
ELSE a.soc END AS soc_start,
CASE WHEN a.chg_state = 'CHARGING' AND NOT (a.st_vor <=> 'CHARGING')
AND a.dt_vor <= $luecke
THEN COALESCE(NULLIF(a.wh_vor, 0), NULLIF(a.wb_wh_total, 0))
ELSE NULLIF(a.wb_wh_total, 0) END AS wh_start,
/*
* Fahrzeugseitige Energie: chg_kw ist die Ladeleistung, die
* das Fahrzeug selbst meldet - gemessen HINTER dem Ladegerät.
* Die API kennt keine Kilowattstunden, deshalb wird hier über
* die Zeit integriert, je Schritt mit dem Mittel aus vorigem
* und aktuellem Wert (Trapez). Beim Laden mit Wechselstrom
* liegt die Leistung fast konstant an, da ist das genau genug.
* chg_kw ist die Ladeleistung, die das Fahrzeug selbst meldet -
* gemessen HINTER dem Ladegerät. Die API kennt keine
* Kilowattstunden, deshalb wird über die Zeit integriert, je
* Schritt mit dem Mittel aus vorigem und aktuellem Wert.
*
* Die Bedingung hält die Summe innerhalb einer Ladung: über
* eine Lücke von mehr als 30 Minuten hinweg wäre die Fläche
* unter der Kurve frei erfunden.
* Über mehr als 30 Minuten ohne Meldung wäre die Fläche unter
* der Kurve erfunden: die Leistung kann in der Zeit von 4 auf
* 10 kW gestiegen sein, ohne dass eine Zeile davon weiß.
* zu_lang merkt sich das, und die Ladung bekommt dann keinen
* fahrzeugseitigen Wert.
*/
CASE WHEN LAG(chg_state) OVER w <=> 'CHARGING'
AND TIMESTAMPDIFF(SECOND, LAG(datetime) OVER w, datetime)
BETWEEN 1 AND 1800
THEN (COALESCE(chg_kw, 0) + COALESCE(LAG(chg_kw) OVER w, 0)) / 2
* TIMESTAMPDIFF(SECOND, LAG(datetime) OVER w, datetime) / 3600
ELSE 0 END AS kwh_teil
FROM skoda WHERE datetime >= $seit
WINDOW w AS (ORDER BY datetime)) s) t
WHERE chg_state = 'CHARGING'
CASE WHEN a.st_vor <=> 'CHARGING' AND a.dt_vor BETWEEN 1 AND $luecke
THEN (COALESCE(a.chg_kw, 0) + COALESCE(a.kw_vor, 0)) / 2
* a.dt_vor / 3600
ELSE 0 END AS kwh_teil,
CASE WHEN a.st_vor <=> 'CHARGING' AND a.dt_vor > 1800
THEN 1 ELSE 0 END AS zu_lang
FROM (
SELECT datetime, soc, chg_state, chg_type, chg_kw, target_soc,
wb_wh_total, pv_kw,
LAG(chg_state) OVER w AS st_vor,
TIMESTAMPDIFF(SECOND, LAG(datetime) OVER w, datetime) AS dt_vor,
LAG(soc) OVER w AS soc_vor,
LAG(wb_wh_total) OVER w AS wh_vor,
LAG(chg_kw) OVER w AS kw_vor
FROM skoda WHERE datetime >= $seit
WINDOW w AS (ORDER BY datetime)) a) b) t
-- Die Ladezeilen und die eine Zeile, mit der die Ladung endet.
WHERE laedt OR weiter
GROUP BY sitzung";
}
@@ -167,6 +213,154 @@ function kapazitaetSchaetzen($mysql)
];
}
/**
* Verlauf einer Ladung: Wallbox-Leistung dicht, Ladestand dazwischen gefüllt.
*
* Das Fahrzeug meldet beim Laden an der Wallbox mitunter nur einmal in der
* Stunde, die Wallbox steht in skoda_ladepunkte im Minutentakt (siehe
* solarLog_skoda_ladepunkte.sql). Der Ladestand zwischen zwei Meldungen wird
* nach der geflossenen Energie aufgefüllt, nicht nach der Zeit:
*
* soc(t) = soc_a + (soc_b - soc_a) * (wh(t) - wh_a) / (wh_b - wh_a)
*
* Das braucht weder Akkukapazität noch Wirkungsgrad. Gegengeprüft am
* 13.09.2026: zwischen 56 % (09:59) und 70 % (11:22) ergibt es für 10:29
* 59,6 % - gemeldet waren 60 %. Nach der Zeit wären es 61,1 % gewesen.
*
* Eine Meldung gilt für den Zeitpunkt, zu dem das Fahrzeug sie erfasst hat
* (cap_chg), nicht für den Abruf - dazwischen können Minuten mit 11 kW
* liegen. Der Zählerstand zu diesem Zeitpunkt kommt deshalb aus dem
* Wallbox-Verlauf selbst und nicht aus der Zeile.
*
* Vor der ersten und nach der letzten Meldung bleibt der Ladestand leer:
* hochgerechnet wird nichts.
*
* Liefert [verlauf, meldungen, wallbox]; ohne Punkte in skoda_ladepunkte
* zwei leere Listen - dann zeichnet die Seite wie früher aus den Zeilen.
*/
function ladeverlauf($mysql, $von, $bis)
{
// Welche Wallbox: die, die im Zeitraum Energie abgegeben hat.
$stmt = $mysql->prepare(
"SELECT wallbox, MAX(wh_total) - MIN(wh_total) AS wh, COUNT(*) AS n
FROM skoda_ladepunkte
WHERE datetime BETWEEN (? - INTERVAL 30 MINUTE) AND (? + INTERVAL 30 MINUTE)
GROUP BY wallbox ORDER BY wh DESC, n DESC LIMIT 1");
if (!$stmt) {
return [[], [], null]; // Tabelle fehlt noch
}
$stmt->bind_param("ss", $von, $bis);
$stmt->execute();
$wahl = $stmt->get_result()->fetch_assoc();
if (!$wahl) {
return [[], [], null];
}
$wallbox = $wahl["wallbox"];
$spalte = $wallbox === "og" ? "wbog_wh_total" : "wb_wh_total";
$stmt = $mysql->prepare(
"SELECT datetime, kw, wh_total, quelle FROM skoda_ladepunkte
WHERE wallbox = ? AND datetime BETWEEN (? - INTERVAL 30 MINUTE) AND (? + INTERVAL 30 MINUTE)
ORDER BY datetime");
$stmt->bind_param("sss", $wallbox, $von, $bis);
$stmt->execute();
$res = $stmt->get_result();
$verlauf = [];
while ($z = $res->fetch_assoc()) {
$verlauf[] = [
"datetime" => $z["datetime"],
"t" => strtotime($z["datetime"]),
"kw" => floatval($z["kw"]),
"wh" => $z["wh_total"] === null ? null : intval($z["wh_total"]),
"quelle" => $z["quelle"],
];
}
if (!$verlauf) {
return [[], [], null];
}
// Zählerstand zu einem beliebigen Zeitpunkt, aus dem Verlauf. Vor dem
// ersten Punkt ruhte die Wallbox - dann gilt der Stand, den die Zeile
// selbst trägt.
$zaehlerBei = function ($t, $ersatz) use ($verlauf) {
$vor = null;
foreach ($verlauf as $p) {
if ($p["wh"] === null) {
continue;
}
if ($p["t"] >= $t) {
if ($vor === null) {
return $ersatz ?: $p["wh"];
}
$spanne = $p["t"] - $vor["t"];
return $spanne > 0
? $vor["wh"] + ($p["wh"] - $vor["wh"]) * ($t - $vor["t"]) / $spanne
: $p["wh"];
}
$vor = $p;
}
return $vor ? $vor["wh"] : $ersatz;
};
// Die Meldungen des Fahrzeugs, die den Verlauf einrahmen: die letzte vor
// dem ersten Punkt, alle dazwischen, die erste nach dem letzten.
$erster = $verlauf[0]["datetime"];
$letzter = end($verlauf)["datetime"];
$stmt = $mysql->prepare(
"(SELECT COALESCE(cap_chg, datetime) AS erfasst, soc, chg_kw, chg_state, $spalte AS wh
FROM skoda WHERE COALESCE(cap_chg, datetime) <= ? AND soc IS NOT NULL
ORDER BY erfasst DESC LIMIT 1)
UNION
(SELECT COALESCE(cap_chg, datetime), soc, chg_kw, chg_state, $spalte
FROM skoda WHERE COALESCE(cap_chg, datetime) > ? AND COALESCE(cap_chg, datetime) < ?
AND soc IS NOT NULL)
UNION
(SELECT COALESCE(cap_chg, datetime), soc, chg_kw, chg_state, $spalte
FROM skoda WHERE COALESCE(cap_chg, datetime) >= ? AND soc IS NOT NULL
ORDER BY 1 LIMIT 1)
ORDER BY erfasst");
$stmt->bind_param("ssss", $erster, $erster, $letzter, $letzter);
$stmt->execute();
$res = $stmt->get_result();
$meldungen = [];
while ($z = $res->fetch_assoc()) {
$t = strtotime($z["erfasst"]);
$meldungen[] = [
"datetime" => $z["erfasst"],
"t" => $t,
"soc" => intval($z["soc"]),
"chg_kw" => $z["chg_kw"] === null ? null : floatval($z["chg_kw"]),
"chg_state"=> $z["chg_state"],
"wh" => $zaehlerBei($t, intval($z["wh"])),
];
}
// Ladestand je Punkt, zwischen den beiden Meldungen, die ihn einrahmen.
foreach ($verlauf as &$p) {
$p["soc"] = null;
for ($i = 1; $p["wh"] !== null && $i < count($meldungen); $i++) {
$a = $meldungen[$i - 1];
$b = $meldungen[$i];
if ($p["t"] < $a["t"] || $p["t"] > $b["t"]) {
continue;
}
$spanne = $b["wh"] - $a["wh"];
$p["soc"] = $spanne > 0
? round($a["soc"] + ($b["soc"] - $a["soc"]) * ($p["wh"] - $a["wh"]) / $spanne, 1)
: floatval($a["soc"]);
break;
}
unset($p["t"]);
}
unset($p);
foreach ($meldungen as &$m) {
unset($m["t"], $m["wh"]);
}
unset($m);
return [$verlauf, $meldungen, $wallbox];
}
/** Median eines nicht leeren Zahlenfeldes. */
function median($werte)
{
@@ -237,6 +431,9 @@ case "kurve":
while ($z = $res->fetch_assoc()) { $antwort["punkte"][] = $z; }
$antwort["von"] = $von;
$antwort["bis"] = $bis;
list($antwort["verlauf"], $antwort["meldungen"], $antwort["wallbox"])
= ladeverlauf($mysql, $von, $bis);
break;
/* ------------------------------------------------------------ gesundheit -- */
+39 -73
View File
@@ -14,28 +14,25 @@
* ein vorausgeladenes Bild oder einen Klick auf eine fremde Seite fernsteuerbar.
*
* ---------------------------------------------------------------------------
* Zwei Schlüssel, zwei Kontingente
* Ein Kontingent für alles
*
* Die API begrenzt auf 20 Anfragen je Stunde **und Schlüssel**, und Befehle
* zählen genauso mit wie Abrufe. Steht in skoda.conf ein eigener
* CMD_API_KEY, hat die Steuerung damit ihr eigenes Kontingent und nimmt dem
* Abruf des Fahrzeugzustands nichts weg. Beide Schlüssel werden in der
* MyŠkoda-App angelegt.
* Die API begrenzt auf 20 Anfragen je Stunde, und Befehle zählen genauso mit
* wie Abrufe. Die Doku schreibt "je Schlüssel" - gemessen stimmt das nicht,
* der Zähler gilt für alle Schlüssel des Kontos zusammen (die Rechnung steht
* bei _LIMIT in gatherSkodaData.py). Mehrere Schlüssel bringen also kein
* zusätzliches Kontingent.
*
* Ohne eigenen Schlüssel greift dieses Skript auf API_KEY zurück - dann
* teilen sich beide ein Kontingent, und weil sie in getrennten Prozessen
* laufen und keinen gemeinsamen Zähler haben, ist es fest aufgeteilt:
* Abruf und Steuerung laufen in getrennten Prozessen ohne gemeinsamen Zähler,
* deshalb ist fest aufgeteilt:
*
* eigener CMD_API_KEY nur API_KEY
* Abruf 18 / Stunde 14 / Stunde
* Befehle 15 / Stunde 4 / Stunde
* Reserve 2 / Stunde 2 / Stunde
* Abruf 14 / Stunde (gatherSkodaData.py)
* Befehle 4 / Stunde
* Reserve 2 / Stunde
*
* gatherSkodaData.py stellt sich selbst um, sobald der zweite Schlüssel in
* skoda.conf auftaucht - dort ist nichts von Hand nachzuziehen.
*
* Der zweite Schlüssel ist kein Freibrief, den Abruf zu beschleunigen: die
* Kachelrichtlinie zielt auf die Last, nicht auf die Buchführung.
* Ein eigener CMD_API_KEY trennt damit nicht die Kontingente, sondern nur,
* wer womit fragt - er lässt sich einzeln widerrufen, ohne den Abruf
* mitzunehmen. Welchen dieses Skript nimmt, entscheidet
* skodaBefehlsSchluessel(): den eigenen, wenn es ihn gibt, sonst API_KEY.
* ---------------------------------------------------------------------------
*
* Nicht angebunden ist die Standheizung. Ihr Start verlangt laut Spec die
@@ -46,24 +43,13 @@
*/
require_once("../helper.php");
// Wo skoda.conf liegt, wie sie aussieht, welcher Schlüssel die Befehle
// schickt und wie viele je Stunde erlaubt sind: alles dort, damit der Reiter
// in den Einstellungen und dieser Endpunkt dieselbe Wahrheit benutzen.
require_once("../restricted/skodaKeys.php");
header('Content-Type: application/json; charset=utf-8');
const API_BASIS = "https://public.api.connect.skoda-auto.cz/api/v1/vehicles/";
const KONFIG = "/volume1/homes/wagner/SolarManager/skoda.conf";
const KENNUNG = "SmartController-SkodaSteuerung/1.0 (+https://nas.el-wa.org/smart)";
/** Befehle je Stunde - siehe Kopfkommentar. Hängt davon ab, ob die
* Steuerung einen eigenen Schlüssel hat oder ihn mit dem Abruf teilt. */
const BEFEHLE_EIGENER_SCHLUESSEL = 15;
const BEFEHLE_GETEILT = 4;
/** Wo der Stundenzähler liegt. Muss für den Webserver beschreibbar sein. */
const ZAEHLER_ORTE = [
"/volume1/web/smart/tiles", // falls per synoacltool freigegeben
"/var/services/tmp/smart-tiles", // sonst
];
/*
* Die erlaubten Befehle. Der Schlüssel kommt aus dem Formular, der Pfad
* niemals - so kann über den Parameter kein beliebiger Endpunkt der API
@@ -93,18 +79,16 @@ if (!isset(BEFEHLE[$name])) {
}
$befehl = BEFEHLE[$name];
$konfig = konfigLesen();
$konfig = skodaBefehlsSchluessel();
if (!$konfig) {
antwort(503, false, "Kein API-Schlüssel hinterlegt - skoda.conf fehlt.");
antwort(503, false, "Kein API-Schlüssel hinterlegt - unter Einstellungen "
. "▸ Fahrzeug lässt sich einer eintragen.");
}
$grenze = $konfig["EIGENER"] ? BEFEHLE_EIGENER_SCHLUESSEL : BEFEHLE_GETEILT;
if (!budgetFrei($grenze)) {
if (!budgetFrei(SKODA_BEFEHLE)) {
antwort(429, false, "Stündliches Kontingent für Befehle erschöpft ("
. $grenze . " je Stunde)."
. ($konfig["EIGENER"] ? "" : " Ein eigener CMD_API_KEY in skoda.conf"
. " würde der Steuerung ein eigenes Kontingent geben.")
. " Später erneut versuchen.");
. SKODA_BEFEHLE . " je Stunde). Es teilt sich die 20 Anfragen der API"
. " mit dem Abruf des Fahrzeugzustands. Später erneut versuchen.");
}
/* ---------------------------------------------------------------- Ausführen */
@@ -121,8 +105,9 @@ if ($name === "klima_start") {
list($status, $inhalt) = senden($konfig, $befehl["pfad"], $rumpf);
if ($status === 202) {
skodaAbrufAnstossen();
antwort(200, true, $befehl["text"] . ". Das Fahrzeug führt den Befehl aus; "
. "in der Anzeige erscheint es beim nächsten Abruf.");
. "in der Anzeige erscheint es in einer halben Minute.");
}
antwort(200, false, fehlertext($status, $inhalt));
@@ -136,39 +121,19 @@ function antwort($code, $ok, $text)
exit;
}
/** API-Schlüssel und VIN aus skoda.conf lesen. Format: NAME=wert je Zeile. */
function konfigLesen()
{
if (!is_readable(KONFIG)) {
return null;
}
$werte = [];
foreach (file(KONFIG, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) 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), "\"'");
}
if (empty($werte["API_KEY"]) || empty($werte["VIN"])) {
return null;
}
// Eigener Schluessel fuer die Steuerung, wenn hinterlegt - sonst der
// gemeinsame. Welcher es wurde, entscheidet unten ueber das Budget.
$werte["EIGENER"] = !empty($werte["CMD_API_KEY"]);
$werte["SCHLUESSEL"] = $werte["EIGENER"] ? $werte["CMD_API_KEY"] : $werte["API_KEY"];
return $werte;
}
/**
* Stundenzähler für Befehle. Gleiches Verfahren wie die Bremse in tile.php:
* eine kleine Datei, gegen gleichzeitige Zugriffe mit flock abgesichert.
*
* Das Fenster ist fest: es beginnt mit dem ersten Befehl und läuft eine Stunde.
* Die API zählt gleitend, hier stehen also im Grenzfall zwei Fenster
* nebeneinander - garantiert sind die $grenze Befehle, mehr geht auf das Risiko
* eines 429 von der API selbst.
*/
function budgetFrei($grenze)
{
$ort = null;
foreach (ZAEHLER_ORTE as $kandidat) {
foreach (SKODA_ARBEITSORTE as $kandidat) {
if (is_dir($kandidat) && is_writable($kandidat)) {
$ort = $kandidat;
break;
@@ -180,7 +145,7 @@ function budgetFrei($grenze)
return true;
}
$griff = fopen($ort . "/.befehle.json", "c+");
$griff = fopen($ort . "/" . SKODA_ZAEHLER_DATEI, "c+");
if (!$griff || !flock($griff, LOCK_EX)) {
return true;
}
@@ -207,16 +172,16 @@ function budgetFrei($grenze)
/** Den Befehl an die API schicken. Liefert [HTTP-Status, Rumpf]. */
function senden($konfig, $pfad, $rumpf)
{
$url = API_BASIS . rawurlencode($konfig["VIN"]) . "/" . $pfad;
$url = SKODA_API . rawurlencode($konfig["vin"]) . "/" . $pfad;
$kopf = [
"X-API-Key: " . $konfig["SCHLUESSEL"],
"X-API-Key: " . $konfig["schluessel"],
"Accept: application/json",
];
$ch = curl_init($url);
$optionen = [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_USERAGENT => KENNUNG,
CURLOPT_USERAGENT => SKODA_KENNUNG,
CURLOPT_TIMEOUT => 20,
CURLOPT_CONNECTTIMEOUT => 5,
];
@@ -257,7 +222,8 @@ function fehlertext($status, $inhalt)
switch ($status) {
case 0: return "Die Škoda-API ist nicht erreichbar.";
case 400: return "Die API hat den Befehl abgelehnt." . $detail;
case 401: return "Der API-Schlüssel ist abgelaufen - in der MyŠkoda-App erneuern.";
case 401: return "Der API-Schlüssel ist abgelaufen - in der MyŠkoda-App "
. "erneuern und unter Einstellungen ▸ Fahrzeug eintragen.";
case 403: return "Der Schlüssel darf diesen Befehl nicht ausführen." . $detail;
case 404: return "Fahrzeug nicht gefunden - stimmt die VIN in skoda.conf?";
case 422: return "Das Fahrzeug kann das gerade nicht." . $detail;