Drei Teile, die zusammengehoeren: die Fahrzeugkachel zeigt den Ladestand jetzt dort, wo er im Auto auch sitzt, die Jahresstatistik wird zur Tabelle, und beide holen ihr Aussehen aus einer gemeinsamen Farbwelt. Fahrzeugkachel. Die Kachel "Batterie und Ladung" ist aufgeloest; ihr Inhalt steckt in der Draufsicht. Die Akkuplatte im Fahrzeugboden fuellt sich von hinten nach vorn, traegt die Zielmarke und nennt Prozent und Reichweite in ihrer Mitte - auf dem Goldenen Schnitt der Plattenhoehe, damit sie nicht am Klimaknopf klebt. Beim Laden wandern Streifen im gefuellten Teil nach vorn, und vom Anschluss fliesst eine Linie in die Platte. Was sich nicht zeichnen laesst - Ladeleistung, Restzeit, Kilometerstand, Klima, Herkunft des Ladestroms - steht als Kennzahl daneben, dazu die gefahrenen Kilometer aus Woche, Monat und Jahr (?was=strecke, gerechnet aus dem Zaehlerstand statt aus erkannten Fahrten - faellt eine Zeile aus, fehlt die Fahrt in der Liste, im Zaehlerstand aber nicht). Am Fahrzeug selbst: kein Panoramadach, das hat unseres nicht. Reling und Spoiler neu gezeichnet, weil sie mit der Dachplatte bisher ein geschlossenes Rechteck bildeten und das wie ein Fensterrahmen aussah. Die Blende am Bug ist schmaler und heller - von oben sieht man von einer senkrechten Flaeche nur einen Streifen, und fast schwarz las sie sich wie ein Loch. Die Ladeklappe ist gebaut wie der Klimaknopf; den Ladezustand traegt jetzt die Farbe statt einer zweiten Form. Jahresvergleich. getStats.php liefert kein fertiges HTML mehr, sondern JSON, das sich selbst beschreibt: Einheit, Nachkommastellen, Richtung und Zugehoerigkeit stehen als Metadaten je Kennzahl, nicht mehr als zwei Arrays, die ueber den Spaltenindex mit der Abfrage synchron gehalten werden mussten. Zwei Abfragen holen alles - Monatswerte, daraus die ganzen Jahre und die Summe seit Aufzeichnungsbeginn, dazu der gleiche Zeitraum jedes Jahres bis zum heutigen Kalendertag. Verhaeltniszahlen sind auf jeder Ebene neu aus den Summen gerechnet; ein Jahresmittel ist nicht das Mittel der Monatsmittel. Gezeichnet wird das von einem Baustein, den sich Historien- und Solarseite teilen: dort drei Jahresspalten mit verschiebbarem Fenster bis zurueck zum ersten Jahr, hier eine Spalte ohne Werkzeuge. Zeilen, die sich aufteilen, stehen als "davon" darunter; was bereits in einer anderen Zeile steckt, als "darin" samt Namen der Zeile - der Beitrag der Batterie und die Solarladung des Autos duerfen nicht zum Ertrag addiert werden. Zusammengefasste Zeilen sind zunaechst zu, damit die Tabelle mit zehn Zeilen aufmacht statt mit siebzehn. Farbwelt. Statt Regel fuer Regel zu ueberschreiben, ist die Palette ausgetauscht: ein paar Variablen am Wurzelelement, an die Bootstrap und AdminLTE ohnehin gehen. Anthrazit als Grund, eine Spur hellere Flaeche fuer Karten, Haarlinien statt Rahmen und Schatten, Bernstein als einzige Akzentfarbe - auf dieser Seite die Farbe der eigenen Anlage. Gruen, Rot und Gelb bleiben den Zustaenden vorbehalten. Erklaerungen, die bisher nur im title standen, sind antippbar: am Finger gibt es kein Ueberfahren, und ein title erscheint dort nie. Die beiden neuen Abfragen sind gegen solarLog gelaufen, die Monatswerte decken sich mit einer unabhaengigen Zerlegung. grundpreisImJahr() ruft jetzt grundpreisImZeitraum() auf - dieselbe Rechnung, nur auch fuer einen Teil des Jahres. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
414 lines
17 KiB
PHP
414 lines
17 KiB
PHP
<?php
|
|
/*
|
|
* Daten der Škoda-Seite. Ein Endpunkt, mehrere Abschnitte:
|
|
*
|
|
* ?was=live letzter bekannter Stand, alle Spalten der jüngsten Zeile
|
|
* ?was=ladungen Ladevorgänge der letzten N Tage, aus Sitzungen gebildet
|
|
* ?was=kurve Verlauf einer einzelnen Ladung (SoC und kW über Zeit)
|
|
* ?was=gesundheit kWh je 100 Ladestandsprozent, je Ladung, mit Trend
|
|
* ?was=fahrten Fahrten zwischen zwei Parkständen, mit Koordinaten
|
|
* ?was=strecke gefahrene Kilometer über Woche, Monat und Jahr
|
|
*
|
|
* Die Fahrzeugdaten stehen in der Tabelle skoda, geschrieben von
|
|
* gatherSkodaData.py. Der Zeilentakt ist nicht fest: geschrieben wird, wenn
|
|
* das Fahrzeug etwas Neues meldet, mindestens stündlich. Alle Auswertungen
|
|
* hier müssen damit umgehen, dass die Abstände zwischen zwei Zeilen von vier
|
|
* Minuten bis zu einer Stunde reichen.
|
|
*
|
|
* Solange kein Fahrzeug angebunden ist, ist die Tabelle leer. Jeder Abschnitt
|
|
* liefert dann eine leere, aber wohlgeformte Antwort - die Seite zeigt
|
|
* daraufhin ihre Leerzustände und läuft nicht in Fehler.
|
|
*/
|
|
|
|
require_once("../helper.php");
|
|
|
|
header('Content-Type: application/json; charset=utf-8');
|
|
|
|
if (!checkLogin()) {
|
|
http_response_code(403);
|
|
echo json_encode(["error" => "Nicht angemeldet."]);
|
|
exit;
|
|
}
|
|
|
|
$was = isset($_GET["was"]) ? $_GET["was"] : "live";
|
|
$tage = isset($_GET["tage"]) ? max(1, min(3650, intval($_GET["tage"]))) : 90;
|
|
|
|
$mysql = new mysqli($mysql_server, $mysql_solarUser, $mysql_solarPass, $mysql_solarDB);
|
|
if ($mysql->connect_errno) {
|
|
http_response_code(500);
|
|
echo json_encode(["error" => "Datenbank nicht erreichbar."]);
|
|
exit;
|
|
}
|
|
$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.
|
|
*
|
|
* 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.
|
|
*/
|
|
function sitzungenSQL($seit)
|
|
{
|
|
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,
|
|
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
|
|
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,
|
|
/*
|
|
* 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.
|
|
*
|
|
* 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.
|
|
*/
|
|
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'
|
|
GROUP BY sitzung";
|
|
}
|
|
|
|
/*
|
|
* Rückfallwert für den Ladewirkungsgrad, solange er sich nicht messen lässt.
|
|
* Beim Laden mit Wechselstrom gehen im Ladegerät des Fahrzeugs grob zehn
|
|
* Prozent verloren.
|
|
*/
|
|
const LADE_WIRKUNGSGRAD_ANNAHME = 0.90;
|
|
|
|
/** Ab so vielen Ladungen gilt der gemessene Wirkungsgrad als belastbar. */
|
|
const WIRKUNGSGRAD_MIN_LADUNGEN = 5;
|
|
|
|
/**
|
|
* Wie viele Kilowattstunden ein Ladestandsprozent wert ist.
|
|
*
|
|
* Zwei Energien je Ladung stehen zur Verfügung, und der Unterschied zwischen
|
|
* ihnen ist genau das, was das Ladegerät verheizt:
|
|
*
|
|
* wh Differenz der Wallbox-Zählerstände - vor dem Ladegerät
|
|
* kwh_fahrzeug chg_kw über die Zeit integriert - hinter dem Ladegerät
|
|
*
|
|
* Für den Fahrverbrauch zählt, was die Batterie verlässt, also die zweite.
|
|
* Sie wird bevorzugt; erst wenn zu wenige Ladungen sie hergeben, wird der
|
|
* Zählerwert mit dem angenommenen Wirkungsgrad heruntergerechnet.
|
|
*
|
|
* Der Median statt des Mittels, weil einzelne Ladungen durch die Rundung des
|
|
* Ladestands weit ausreissen können.
|
|
*
|
|
* Gibt null zurück, solange es zu wenige Ladungen gibt. Die Oberfläche zeigt
|
|
* dann keine Kilowattstunden an, statt eine erfundene Zahl.
|
|
*/
|
|
function kapazitaetSchaetzen($mysql)
|
|
{
|
|
$sql = "SELECT ROUND(wh/1000/zuwachs*100, 3) AS je100_zaehler,
|
|
CASE WHEN kwh_fahrzeug > 0
|
|
THEN ROUND(kwh_fahrzeug/zuwachs*100, 3) END AS je100_fahrzeug
|
|
FROM (" . sitzungenSQL("NOW() - INTERVAL 365 DAY") . ") v
|
|
WHERE zuwachs >= 20 AND wh > 1000";
|
|
$res = $mysql->query($sql);
|
|
if (!$res) {
|
|
return null;
|
|
}
|
|
$zaehler = [];
|
|
$fahrzeug = [];
|
|
$paare = []; // beide Werte derselben Ladung, für den Wirkungsgrad
|
|
while ($z = $res->fetch_assoc()) {
|
|
$zaehler[] = floatval($z["je100_zaehler"]);
|
|
if ($z["je100_fahrzeug"] !== null) {
|
|
$fahrzeug[] = floatval($z["je100_fahrzeug"]);
|
|
$paare[] = floatval($z["je100_fahrzeug"]) / floatval($z["je100_zaehler"]);
|
|
}
|
|
}
|
|
if (count($zaehler) < 3) {
|
|
return null;
|
|
}
|
|
|
|
$gemessen = count($fahrzeug) >= WIRKUNGSGRAD_MIN_LADUNGEN;
|
|
if ($gemessen) {
|
|
$netto = median($fahrzeug);
|
|
$grad = median($paare);
|
|
} else {
|
|
$netto = median($zaehler) * LADE_WIRKUNGSGRAD_ANNAHME;
|
|
$grad = LADE_WIRKUNGSGRAD_ANNAHME;
|
|
}
|
|
|
|
return [
|
|
"je100_zaehler" => round(median($zaehler), 2),
|
|
"je100_netto" => round($netto, 2),
|
|
"wirkungsgrad" => round($grad, 3),
|
|
"gemessen" => $gemessen,
|
|
"je_soc_netto" => round($netto / 100, 4),
|
|
"n" => count($zaehler),
|
|
"n_fahrzeug" => count($fahrzeug),
|
|
];
|
|
}
|
|
|
|
/** Median eines nicht leeren Zahlenfeldes. */
|
|
function median($werte)
|
|
{
|
|
sort($werte);
|
|
return $werte[intdiv(count($werte), 2)];
|
|
}
|
|
|
|
$antwort = [];
|
|
|
|
switch ($was) {
|
|
|
|
/* ------------------------------------------------------------------ live -- */
|
|
case "live":
|
|
$res = $mysql->query("SELECT * FROM skoda ORDER BY id DESC LIMIT 1");
|
|
$zeile = $res ? $res->fetch_assoc() : null;
|
|
$antwort = ["stand" => $zeile];
|
|
|
|
// Wie lange schweigt das Fahrzeug schon? Sagt mehr als der Zeitstempel
|
|
// allein, weil die Seite auch aus dem Zwischenspeicher kommen kann.
|
|
if ($zeile) {
|
|
$res = $mysql->query("SELECT TIMESTAMPDIFF(SECOND, '"
|
|
. $mysql->real_escape_string($zeile["datetime"]) . "', NOW()) AS alter_s");
|
|
$r = $res ? $res->fetch_assoc() : null;
|
|
$antwort["alter_s"] = $r ? intval($r["alter_s"]) : null;
|
|
}
|
|
|
|
// Kennzahlen über den gesamten Bestand, für die Kopfzeile.
|
|
$res = $mysql->query("SELECT COUNT(*) AS zeilen, MIN(datetime) AS seit,
|
|
MAX(odo_km) AS km FROM skoda");
|
|
$antwort["bestand"] = $res ? $res->fetch_assoc() : null;
|
|
break;
|
|
|
|
/* -------------------------------------------------------------- ladungen -- */
|
|
case "ladungen":
|
|
$sql = "SELECT * FROM (" . sitzungenSQL("NOW() - INTERVAL $tage DAY") . ") v
|
|
ORDER BY beginn DESC LIMIT 200";
|
|
$res = $mysql->query($sql);
|
|
$antwort["ladungen"] = [];
|
|
while ($res && $z = $res->fetch_assoc()) {
|
|
$antwort["ladungen"][] = $z;
|
|
}
|
|
break;
|
|
|
|
/* ----------------------------------------------------------------- kurve -- */
|
|
case "kurve":
|
|
// Verlauf einer Ladung. Ohne Parameter die jüngste.
|
|
$von = isset($_GET["von"]) ? $_GET["von"] : null;
|
|
$bis = isset($_GET["bis"]) ? $_GET["bis"] : null;
|
|
if (!$von || !$bis) {
|
|
$sql = "SELECT beginn, ende FROM (" . sitzungenSQL("NOW() - INTERVAL 365 DAY") . ") v
|
|
ORDER BY beginn DESC LIMIT 1";
|
|
$res = $mysql->query($sql);
|
|
$z = $res ? $res->fetch_assoc() : null;
|
|
if (!$z) { $antwort["punkte"] = []; break; }
|
|
$von = $z["beginn"];
|
|
$bis = $z["ende"];
|
|
}
|
|
// Eine Zeile Vorlauf und Nachlauf, damit die Kurve nicht in der Luft endet.
|
|
$stmt = $mysql->prepare(
|
|
"SELECT datetime, soc, chg_kw, wb_kw, pv_kw, chg_state, wb_wh_total
|
|
FROM skoda
|
|
WHERE datetime BETWEEN (? - INTERVAL 15 MINUTE) AND (? + INTERVAL 15 MINUTE)
|
|
ORDER BY datetime");
|
|
$stmt->bind_param("ss", $von, $bis);
|
|
$stmt->execute();
|
|
$res = $stmt->get_result();
|
|
$antwort["punkte"] = [];
|
|
while ($z = $res->fetch_assoc()) { $antwort["punkte"][] = $z; }
|
|
$antwort["von"] = $von;
|
|
$antwort["bis"] = $bis;
|
|
break;
|
|
|
|
/* ------------------------------------------------------------ gesundheit -- */
|
|
case "gesundheit":
|
|
/*
|
|
* Kilowattstunden je hundert Ladestandsprozent. Der Wert liegt über der
|
|
* nutzbaren Kapazität, weil die Ladeverluste mitgezählt werden - absolut
|
|
* also nur eine Näherung, sein Verlauf über die Jahre dagegen die
|
|
* eigentliche Aussage.
|
|
*
|
|
* Nur lange Ladungen an der eigenen Wallbox taugen dazu: bei zwanzig
|
|
* Prozentpunkten Zuwachs wiegt ein gerundeter Ladestandspunkt fünf
|
|
* Prozent, und eine Schnellladung unterwegs hat auf unserer Seite keinen
|
|
* Zähler.
|
|
*/
|
|
$sql = "SELECT beginn, soc_von, soc_bis, zuwachs, art, wh,
|
|
ROUND(wh/1000/zuwachs*100, 2) AS je100
|
|
FROM (" . sitzungenSQL("NOW() - INTERVAL $tage DAY") . ") v
|
|
WHERE zuwachs >= 20 AND wh > 1000
|
|
ORDER BY beginn";
|
|
$res = $mysql->query($sql);
|
|
$antwort["ladungen"] = [];
|
|
while ($res && $z = $res->fetch_assoc()) { $antwort["ladungen"][] = $z; }
|
|
|
|
// Ausgleichsgerade über die Einzelwerte, x in Jahren. Die Steigung sagt,
|
|
// ob der Wert wandert; ihr Fehler, ab welcher Alterung man überhaupt
|
|
// etwas sehen könnte.
|
|
$n = count($antwort["ladungen"]);
|
|
$antwort["trend"] = null;
|
|
if ($n >= 6) {
|
|
$t0 = strtotime($antwort["ladungen"][0]["beginn"]);
|
|
$xs = []; $ys = [];
|
|
foreach ($antwort["ladungen"] as $l) {
|
|
$xs[] = (strtotime($l["beginn"]) - $t0) / (365.25 * 86400);
|
|
$ys[] = floatval($l["je100"]);
|
|
}
|
|
$mx = array_sum($xs) / $n;
|
|
$my = array_sum($ys) / $n;
|
|
$sxx = 0; $sxy = 0;
|
|
for ($i = 0; $i < $n; $i++) {
|
|
$sxx += ($xs[$i] - $mx) ** 2;
|
|
$sxy += ($xs[$i] - $mx) * ($ys[$i] - $my);
|
|
}
|
|
if ($sxx > 0) {
|
|
$steigung = $sxy / $sxx;
|
|
$achse = $my - $steigung * $mx;
|
|
$rest = 0;
|
|
for ($i = 0; $i < $n; $i++) {
|
|
$rest += ($ys[$i] - ($achse + $steigung * $xs[$i])) ** 2;
|
|
}
|
|
$streu = $n > 2 ? sqrt($rest / ($n - 2)) : 0;
|
|
$fehler = $streu / sqrt($sxx);
|
|
$antwort["trend"] = [
|
|
"steigung" => round($steigung, 3),
|
|
"fehler" => round($fehler, 3),
|
|
"achse" => round($achse, 2),
|
|
"prozent" => $achse != 0 ? round(100 * $steigung / $achse, 2) : null,
|
|
"nachweisbar" => $achse != 0 ? round(100 * 2 * $fehler / $achse, 1) : null,
|
|
"spanne_jahre" => round(max($xs), 2),
|
|
];
|
|
}
|
|
}
|
|
|
|
// Median je Jahr - der Stand, um den die Wolke streut.
|
|
$jahre = [];
|
|
foreach ($antwort["ladungen"] as $l) {
|
|
$j = substr($l["beginn"], 0, 4);
|
|
$jahre[$j][] = floatval($l["je100"]);
|
|
}
|
|
$antwort["jahre"] = [];
|
|
foreach ($jahre as $j => $werte) {
|
|
sort($werte);
|
|
$k = count($werte);
|
|
$antwort["jahre"][] = [
|
|
"jahr" => $j,
|
|
"n" => $k,
|
|
"q1" => round($werte[intdiv($k, 4)], 2),
|
|
"median" => round($werte[intdiv($k, 2)], 2),
|
|
"q3" => round($werte[intdiv(3 * $k, 4)], 2),
|
|
];
|
|
}
|
|
break;
|
|
|
|
/* --------------------------------------------------------------- fahrten -- */
|
|
case "fahrten":
|
|
/*
|
|
* Eine Fahrt liegt zwischen zwei Parkständen. Die API liefert nur die
|
|
* letzte bekannte PARKposition - fährt das Fahrzeug, kommt allein
|
|
* IN_MOTION ohne Koordinaten. Start und Ziel sind damit bekannt, die
|
|
* Strecke dazwischen nicht.
|
|
*
|
|
* CAST(... AS SIGNED) ist nötig: soc und odo_km sind vorzeichenlos, und
|
|
* eine Differenz unter null bricht die Abfrage sonst ab - genau das
|
|
* passiert, sobald zwischen zwei Ständen geladen wurde.
|
|
*/
|
|
$sql = "SELECT * FROM (
|
|
SELECT datetime AS ankunft, address AS nach, lat, lon, odo_km, soc,
|
|
LAG(datetime) OVER w AS abfahrt,
|
|
LAG(address) OVER w AS von,
|
|
LAG(lat) OVER w AS von_lat,
|
|
LAG(lon) OVER w AS von_lon,
|
|
CAST(odo_km AS SIGNED) - LAG(CAST(odo_km AS SIGNED)) OVER w AS km,
|
|
LAG(CAST(soc AS SIGNED)) OVER w - CAST(soc AS SIGNED) AS soc_ab
|
|
FROM skoda
|
|
WHERE park_state = 'PARKED' AND odo_km IS NOT NULL
|
|
AND datetime >= NOW() - INTERVAL $tage DAY
|
|
WINDOW w AS (ORDER BY datetime)) f
|
|
WHERE km > 0
|
|
ORDER BY ankunft DESC LIMIT 100";
|
|
$res = $mysql->query($sql);
|
|
$antwort["fahrten"] = [];
|
|
while ($res && $z = $res->fetch_assoc()) { $antwort["fahrten"][] = $z; }
|
|
|
|
// Aktueller Standort, für die Karte.
|
|
$res = $mysql->query("SELECT datetime, lat, lon, address, park_state
|
|
FROM skoda WHERE lat IS NOT NULL
|
|
ORDER BY id DESC LIMIT 1");
|
|
$antwort["standort"] = $res ? $res->fetch_assoc() : null;
|
|
|
|
$antwort["kapazitaet"] = kapazitaetSchaetzen($mysql);
|
|
break;
|
|
|
|
/* --------------------------------------------------------------- strecke -- */
|
|
case "strecke":
|
|
/*
|
|
* Gefahrene Kilometer über mehrere Zeiträume, gerechnet aus dem
|
|
* Kilometerstand: der Zähler läuft nur vorwärts, also ist die Differenz
|
|
* zwischen dem ältesten Stand im Zeitraum und dem jüngsten überhaupt die
|
|
* gefahrene Strecke. Das ist belastbarer als eine Summe über die
|
|
* erkannten Fahrten - fällt eine Zeile zwischendurch aus, fehlt die
|
|
* Fahrt in der Liste, im Zählerstand aber nicht.
|
|
*
|
|
* CAST(... AS SIGNED) ist nötig: odo_km ist vorzeichenlos, und eine
|
|
* Differenz unter null bräche die Abfrage ab.
|
|
*/
|
|
$zeitraeume = ["woche" => 7, "monat" => 30, "jahr" => 365];
|
|
|
|
$spalten = [];
|
|
foreach ($zeitraeume as $name => $spanne) {
|
|
$spalten[] = "MIN(CASE WHEN datetime >= NOW() - INTERVAL $spanne DAY
|
|
THEN CAST(odo_km AS SIGNED) END) AS von_$name";
|
|
}
|
|
$sql = "SELECT MAX(CAST(odo_km AS SIGNED)) AS jetzt, MIN(datetime) AS seit, "
|
|
. implode(", ", $spalten) . " FROM skoda WHERE odo_km IS NOT NULL";
|
|
$res = $mysql->query($sql);
|
|
$z = $res ? $res->fetch_assoc() : null;
|
|
|
|
$jetzt = ($z && $z["jetzt"] !== null) ? intval($z["jetzt"]) : null;
|
|
$antwort["odo"] = $jetzt;
|
|
$antwort["seit"] = $z ? $z["seit"] : null;
|
|
$antwort["strecke"] = [];
|
|
|
|
foreach ($zeitraeume as $name => $spanne) {
|
|
$von = ($z && $z["von_$name"] !== null) ? intval($z["von_$name"]) : null;
|
|
$antwort["strecke"][$name] = [
|
|
"tage" => $spanne,
|
|
"km" => ($von === null || $jetzt === null) ? null : $jetzt - $von,
|
|
/*
|
|
* Reicht die Aufzeichnung überhaupt so weit zurück? Ein Jahreswert
|
|
* aus drei Monaten Daten ist kein Jahreswert - die Anzeige muss
|
|
* das sagen dürfen, statt eine zu kleine Zahl als Jahr auszugeben.
|
|
*/
|
|
"vollstaendig" => ($z && $z["seit"] !== null)
|
|
&& strtotime($z["seit"]) <= time() - $spanne * 86400
|
|
];
|
|
}
|
|
break;
|
|
|
|
default:
|
|
http_response_code(400);
|
|
$antwort = ["error" => "Unbekannter Abschnitt."];
|
|
}
|
|
|
|
echo json_encode($antwort, JSON_UNESCAPED_UNICODE);
|