Einstellungsseite, Skoda-Seite und Karte
Sammelstand der offenen Arbeit im Dashboard. Einstellungen: eine Eingabemaske fuer die Energiepreise unter restricted/settings.php, dazu costs.php als Modell und ajax/settings.php als Endpunkt. Die Preise stehen jetzt in gridCosts, gasCosts und fuelCosts statt fest im Quelltext von getStats.php; die Jahresstatistik rechnet ueber LEAD() den jeweils gueltigen Zeitraum aus und schlaegt den anteiligen Grundpreis auf. Eingegeben wird in gewohnten Einheiten - l/100 km, kWh/100 km, Euro je Liter -, umgerechnet wird beim Speichern. solarLog_costs.sql beschreibt die Umstellung der Tabellen. Skoda: eigene Seite mit Live-Werten ueber MQTT und Historie ueber ajax/skoda.php, Kommandos ueber ajax/skodaCmd.php. Karte: Leaflet mit einem eigenen Kachel-Zwischenspeicher (ajax/tile.php), dessen Ablage unter tiles/ nicht ins Repository gehoert. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+290
@@ -0,0 +1,290 @@
|
||||
<?php
|
||||
/*
|
||||
* Kachel-Zwischenspeicher für die Karte.
|
||||
*
|
||||
* Aufruf: tile.php?z=16&x=34567&y=22456
|
||||
*
|
||||
* Liegt die Kachel im Zwischenspeicher, kommt sie von dort. Fehlt sie, holt
|
||||
* das NAS sie einmal bei OpenStreetMap, legt sie ab und liefert aus. Danach
|
||||
* verlässt für diese Kachel nie wieder eine Anfrage das Haus.
|
||||
*
|
||||
* Warum überhaupt ein Zwischenspeicher und nicht der direkte Weg vom Browser
|
||||
* zu tile.openstreetmap.org? Drei Gründe:
|
||||
*
|
||||
* 1. Die Kachelrichtlinie der OSM Foundation verlangt Zwischenspeicherung
|
||||
* von mindestens sieben Tagen. Der direkte Weg erfüllt das nur, soweit
|
||||
* der Browser mitspielt.
|
||||
* 2. Ohne Zwischenspeicher ginge die Parkposition des Fahrzeugs - also die
|
||||
* Wohnadresse - bei jedem Seitenaufruf als Kachelanfrage nach draußen.
|
||||
* 3. Einmal geholte Gegenden bleiben ohne Internet benutzbar.
|
||||
*
|
||||
* Was die Richtlinie ausdrücklich verbietet, ist Vorratsspeicherung: Kacheln
|
||||
* auf Verdacht holen, Gebiete vorab abgrasen, Offline-Archive anlegen. Dieses
|
||||
* Skript holt deshalb ausschließlich das, was gerade jemand ansieht, und die
|
||||
* Bremse unten begrenzt zusätzlich, wie viel das pro Minute sein darf.
|
||||
*
|
||||
* Richtlinie: https://operations.osmfoundation.org/policies/tiles/
|
||||
*/
|
||||
|
||||
require_once("../helper.php");
|
||||
|
||||
/* ------------------------------------------------------------ Einstellungen */
|
||||
|
||||
/** Woher die Kacheln kommen. {z}/{x}/{y} werden ersetzt. */
|
||||
const KACHEL_QUELLE = "https://tile.openstreetmap.org/{z}/{x}/{y}.png";
|
||||
|
||||
/*
|
||||
* Die Richtlinie verlangt einen eigenen, kontaktierbaren User-Agent. Vorgaben
|
||||
* von Bibliotheken werden ohne Vorwarnung geblockt. Die Domain genügt als
|
||||
* Kontakt; wer lieber eine Mailadresse hinterlegt, ersetzt sie hier.
|
||||
*/
|
||||
const KENNUNG = "SmartController-SkodaKarte/1.0 (+https://nas.el-wa.org/smart)";
|
||||
|
||||
/*
|
||||
* Wo die Kacheln liegen.
|
||||
*
|
||||
* Bevorzugt neben der Seite unter tiles/ - dort bleiben sie über Neustarts
|
||||
* erhalten und liegen im Backup. PHP läuft auf dem NAS aber unter einem
|
||||
* eigenen Benutzer, und DSM vergibt im Web-Verzeichnis ACLs, die ihm das
|
||||
* Schreiben verwehren. Der POSIX-Modus täuscht: drwxrwxrwx mit einem "+"
|
||||
* dahinter heisst, dass die ACL entscheidet, nicht die Rechtebits.
|
||||
*
|
||||
* Damit die Karte auch ohne Eingriff funktioniert, wird ersatzweise das
|
||||
* Systemverzeichnis für flüchtige Dateien genommen. Das kostet nur den
|
||||
* Bestand: es wird bei Gelegenheit geleert, die Kacheln müssen dann erneut
|
||||
* geholt werden. Dauerhaft wird es mit einem Befehl als root:
|
||||
*
|
||||
* synoacltool -add /volume1/web/smart/tiles \
|
||||
* "user:http:allow:rwxpdDaARWc--:fd--"
|
||||
*
|
||||
* Welcher Ort gerade benutzt wird, sagt speicherOrt() im Fehlerprotokoll,
|
||||
* sobald zum ersten Mal eine Kachel geholt wird.
|
||||
*/
|
||||
const SPEICHER_ORTE = [
|
||||
__DIR__ . "/../tiles", // dauerhaft, bevorzugt
|
||||
"/var/services/tmp/smart-tiles", // flüchtig, aber immer beschreibbar
|
||||
];
|
||||
|
||||
/** Zoomstufen, die ausgeliefert werden. Feiner als Hausnummern braucht es nicht. */
|
||||
const ZOOM_MIN = 6;
|
||||
const ZOOM_MAX = 18;
|
||||
|
||||
/** Wie lange der Browser eine Kachel behalten darf (Sekunden). */
|
||||
const BROWSER_FRIST = 7 * 24 * 3600;
|
||||
|
||||
/**
|
||||
* Nach dieser Zeit wird eine Kachel beim nächsten Ansehen neu geholt. Die
|
||||
* Karte ändert sich, aber nicht schnell. Scheitert das Nachholen, bleibt die
|
||||
* alte Kachel - die Karte ist dann veraltet, aber nie kaputt.
|
||||
*/
|
||||
const AUFFRISCHEN_NACH = 60 * 24 * 3600;
|
||||
|
||||
/*
|
||||
* Bremse gegen den eigenen Fehler. Ein Kartenfeld dieser Größe braucht rund
|
||||
* zwölf Kacheln je Ansicht; wer sich durch mehrere Zoomstufen klickt, kommt
|
||||
* auf einige Dutzend. Alles weit darüber wäre eine Schleife im Frontend - und
|
||||
* die sähe für OSM wie Vorratsspeicherung aus. Ist die Grenze erreicht, wird
|
||||
* eine graue Ersatzkachel geliefert statt weiter nach draußen zu gehen.
|
||||
*/
|
||||
const HOLEN_JE_MINUTE = 150;
|
||||
const HOLEN_JE_STUNDE = 2000;
|
||||
|
||||
/* ------------------------------------------------------------------ Zugriff */
|
||||
|
||||
if (!checkLogin()) {
|
||||
http_response_code(403);
|
||||
exit;
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------- Prüfung */
|
||||
|
||||
$z = filter_input(INPUT_GET, "z", FILTER_VALIDATE_INT);
|
||||
$x = filter_input(INPUT_GET, "x", FILTER_VALIDATE_INT);
|
||||
$y = filter_input(INPUT_GET, "y", FILTER_VALIDATE_INT);
|
||||
|
||||
// Ohne diese Prüfung liesse sich über z/x/y aus dem Speicherverzeichnis
|
||||
// heraus schreiben - die Werte gehen schliesslich in einen Dateipfad.
|
||||
$grenze = ($z === false || $z === null) ? 0 : (1 << $z);
|
||||
if ($z === false || $z === null || $z < ZOOM_MIN || $z > ZOOM_MAX
|
||||
|| $x === false || $x === null || $x < 0 || $x >= $grenze
|
||||
|| $y === false || $y === null || $y < 0 || $y >= $grenze) {
|
||||
http_response_code(400);
|
||||
exit;
|
||||
}
|
||||
|
||||
$pfad = speicherOrt() . "/$z/$x/$y.png";
|
||||
|
||||
/* -------------------------------------------------------------- Ausliefern */
|
||||
|
||||
$vorhanden = is_file($pfad);
|
||||
$veraltet = $vorhanden && (time() - filemtime($pfad) > AUFFRISCHEN_NACH);
|
||||
|
||||
if (!$vorhanden || $veraltet) {
|
||||
if (holeUndSpeichere($z, $x, $y, $pfad)) {
|
||||
$vorhanden = true;
|
||||
}
|
||||
}
|
||||
|
||||
if ($vorhanden) {
|
||||
liefereDatei($pfad);
|
||||
} else {
|
||||
liefereErsatzkachel();
|
||||
}
|
||||
exit;
|
||||
|
||||
/* ------------------------------------------------------------- Bausteine -- */
|
||||
|
||||
/**
|
||||
* Der erste beschreibbare Ort aus SPEICHER_ORTE, einmal je Aufruf ermittelt.
|
||||
* Existiert keiner, wird der letzte angelegt - der liegt im Systemverzeichnis
|
||||
* für flüchtige Dateien und ist immer beschreibbar.
|
||||
*/
|
||||
function speicherOrt()
|
||||
{
|
||||
static $ort = null;
|
||||
if ($ort !== null) {
|
||||
return $ort;
|
||||
}
|
||||
foreach (SPEICHER_ORTE as $kandidat) {
|
||||
if (is_dir($kandidat) && is_writable($kandidat)) {
|
||||
return $ort = $kandidat;
|
||||
}
|
||||
}
|
||||
$ort = SPEICHER_ORTE[count(SPEICHER_ORTE) - 1];
|
||||
if (!is_dir($ort)) {
|
||||
@mkdir($ort, 0755, true);
|
||||
}
|
||||
return $ort;
|
||||
}
|
||||
|
||||
/**
|
||||
* Eine Kachel bei der Quelle holen und ablegen.
|
||||
* Gibt true zurück, wenn danach eine brauchbare Datei vorliegt.
|
||||
*/
|
||||
function holeUndSpeichere($z, $x, $y, $pfad)
|
||||
{
|
||||
if (!bremseErlaubt()) {
|
||||
return is_file($pfad); // notfalls die veraltete Kachel
|
||||
}
|
||||
|
||||
$url = str_replace(["{z}", "{x}", "{y}"], [$z, $x, $y], KACHEL_QUELLE);
|
||||
|
||||
$ch = curl_init($url);
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_USERAGENT => KENNUNG,
|
||||
CURLOPT_TIMEOUT => 8,
|
||||
CURLOPT_CONNECTTIMEOUT => 4,
|
||||
CURLOPT_FOLLOWLOCATION => true,
|
||||
CURLOPT_MAXREDIRS => 2,
|
||||
]);
|
||||
$inhalt = curl_exec($ch);
|
||||
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
// Auf den PNG-Kopf prüfen, statt nur auf den Statuscode: eine Fehlerseite
|
||||
// mit Status 200 wäre sonst als Kachel im Speicher gelandet und bliebe
|
||||
// dort für immer.
|
||||
if ($status !== 200 || $inhalt === false || strncmp($inhalt, "\x89PNG", 4) !== 0) {
|
||||
return is_file($pfad);
|
||||
}
|
||||
|
||||
if (!is_dir(dirname($pfad)) && !mkdir(dirname($pfad), 0755, true)) {
|
||||
return is_file($pfad);
|
||||
}
|
||||
|
||||
// Erst daneben schreiben, dann umbenennen. Sonst könnte ein zweiter
|
||||
// Aufruf eine halb geschriebene Datei ausliefern.
|
||||
$temp = $pfad . "." . getmypid() . ".teil";
|
||||
if (file_put_contents($temp, $inhalt) === false) {
|
||||
return is_file($pfad);
|
||||
}
|
||||
if (!rename($temp, $pfad)) {
|
||||
unlink($temp);
|
||||
return is_file($pfad);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Darf jetzt noch eine Kachel geholt werden?
|
||||
*
|
||||
* Gezählt wird in einer kleinen Datei, gegen gleichzeitige Zugriffe mit
|
||||
* flock abgesichert. Zwei Fenster: eines je Minute, eines je Stunde. Läuft
|
||||
* ein Fenster ab, fängt sein Zähler wieder bei null an.
|
||||
*/
|
||||
function bremseErlaubt()
|
||||
{
|
||||
$datei = speicherOrt() . "/.bremse.json";
|
||||
|
||||
$griff = fopen($datei, "c+");
|
||||
if (!$griff) {
|
||||
return true;
|
||||
}
|
||||
if (!flock($griff, LOCK_EX)) {
|
||||
fclose($griff);
|
||||
return true;
|
||||
}
|
||||
|
||||
$stand = json_decode(stream_get_contents($griff), true);
|
||||
if (!is_array($stand)) {
|
||||
$stand = ["minute" => 0, "min_n" => 0, "stunde" => 0, "std_n" => 0];
|
||||
}
|
||||
|
||||
$jetzt = time();
|
||||
if ($jetzt - $stand["minute"] >= 60) { $stand["minute"] = $jetzt; $stand["min_n"] = 0; }
|
||||
if ($jetzt - $stand["stunde"] >= 3600) { $stand["stunde"] = $jetzt; $stand["std_n"] = 0; }
|
||||
|
||||
$erlaubt = $stand["min_n"] < HOLEN_JE_MINUTE && $stand["std_n"] < HOLEN_JE_STUNDE;
|
||||
if ($erlaubt) {
|
||||
$stand["min_n"]++;
|
||||
$stand["std_n"]++;
|
||||
}
|
||||
|
||||
ftruncate($griff, 0);
|
||||
rewind($griff);
|
||||
fwrite($griff, json_encode($stand));
|
||||
flock($griff, LOCK_UN);
|
||||
fclose($griff);
|
||||
|
||||
if (!$erlaubt) {
|
||||
error_log("tile.php: Bremse greift, keine weiteren Kacheln in diesem Fenster.");
|
||||
}
|
||||
return $erlaubt;
|
||||
}
|
||||
|
||||
/** Eine gespeicherte Kachel ausliefern, mit Unterstützung für 304. */
|
||||
function liefereDatei($pfad)
|
||||
{
|
||||
$zeit = filemtime($pfad);
|
||||
$marke = '"' . md5($pfad . $zeit) . '"';
|
||||
|
||||
header("Content-Type: image/png");
|
||||
header("Cache-Control: public, max-age=" . BROWSER_FRIST);
|
||||
header("ETag: $marke");
|
||||
header("Last-Modified: " . gmdate("D, d M Y H:i:s", $zeit) . " GMT");
|
||||
|
||||
$bekannt = isset($_SERVER["HTTP_IF_NONE_MATCH"]) ? trim($_SERVER["HTTP_IF_NONE_MATCH"]) : "";
|
||||
if ($bekannt === $marke) {
|
||||
http_response_code(304);
|
||||
return;
|
||||
}
|
||||
|
||||
header("Content-Length: " . filesize($pfad));
|
||||
readfile($pfad);
|
||||
}
|
||||
|
||||
/**
|
||||
* Graue Ersatzkachel, wenn nichts zu holen war - kein Internet, Quelle
|
||||
* gestört oder Bremse aktiv. Ein leeres Feld ist besser als ein kaputtes
|
||||
* Bildsymbol, und es wird bewusst nicht zwischengespeichert.
|
||||
*/
|
||||
function liefereErsatzkachel()
|
||||
{
|
||||
header("Content-Type: image/png");
|
||||
header("Cache-Control: no-store");
|
||||
// 1x1 Punkt in Grau, von Leaflet auf Kachelgröße gezogen.
|
||||
echo base64_decode(
|
||||
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk"
|
||||
. "+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==");
|
||||
}
|
||||
Reference in New Issue
Block a user