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:
@@ -0,0 +1,344 @@
|
||||
<?php
|
||||
/**
|
||||
* Modell-Schicht fuer die Preistabellen der Einstellungsseite.
|
||||
*
|
||||
* Eine Preistabelle ist eine Zeitreihe: ab einem Stichtag gelten neue Preise,
|
||||
* bis der naechste Eintrag beginnt. Mehr steht dort auch nicht - insbesondere
|
||||
* kein Enddatum. Das gab es frueher als eigene Spalte, und weil es eigenstaendig
|
||||
* gepflegt wurde, konnte es etwas anderes behaupten als der naechste Stichtag:
|
||||
* in gridCosts liefen zwei Zeilen ueber das erste Quartal 2023, der JOIN in
|
||||
* ajax/getStats.php fand fuer jeden Messwert zwei Preise, und der
|
||||
* Stromverbrauch 2023 stand mit 2527 statt 1674 kWh in der Statistik.
|
||||
*
|
||||
* Ohne die Spalte kann das nicht mehr passieren: zwei Zeitraeume koennen sich
|
||||
* nicht ueberlappen, weil der eine endet, wo der andere beginnt, und eine
|
||||
* Luecke kann es auch nicht geben. Die juengste Zeile hat kein Ende und gilt
|
||||
* weiter, bis ein neuer Preis eingetragen wird - so verhaelt sich ein Tarif ja
|
||||
* auch. Wo die Statistik das Ende braucht, holt sie es sich mit
|
||||
* LEAD(active_date) beim naechsten Stichtag.
|
||||
*
|
||||
* Was bleibt: vor dem aeltesten Stichtag gibt es keinen Preis. Messwerte, die
|
||||
* davor liegen, fallen aus der Statistik heraus - dagegen hilft nur hinsehen,
|
||||
* siehe preisLuecke().
|
||||
*/
|
||||
|
||||
require_once(__DIR__ . "/mysql.php");
|
||||
|
||||
/** Verbindung zur Messwert-Datenbank. */
|
||||
function solarDb()
|
||||
{
|
||||
static $db = null;
|
||||
if ($db === null) {
|
||||
$db = new mysqli($GLOBALS["mysql_server"], $GLOBALS["mysql_solarUser"],
|
||||
$GLOBALS["mysql_solarPass"], $GLOBALS["mysql_solarDB"]);
|
||||
$db->set_charset("utf8mb4");
|
||||
}
|
||||
return $db;
|
||||
}
|
||||
|
||||
/**
|
||||
* Die bearbeitbaren Preistabellen.
|
||||
*
|
||||
* Je Feld:
|
||||
* faktor rechnet zwischen Speicher- und Anzeigeeinheit um. Gespeichert
|
||||
* wird in Euro, angezeigt und eingegeben in Cent - so, wie es auf
|
||||
* der Rechnung steht. 0,3163 EUR/kWh sind 31,63 ct/kWh; niemand
|
||||
* tippt gern vier Nachkommastellen. Vorgabe 1, also unveraendert.
|
||||
* stellen Nachkommastellen der Anzeige, zugleich die Schrittweite im Feld.
|
||||
* max Obergrenze. Was darueber liegt, ist ein Tippfehler und keine
|
||||
* Preiserhoehung.
|
||||
*
|
||||
* Nicht jedes Feld ist ein Preis. Der Energieinhalt des Gases stand frueher
|
||||
* als 6.58 im SQL, die Benzinersparnis als Teiler 24.5 - beides gehoert neben
|
||||
* den Preis, weil es am Brennstoff und am Fahrzeug haengt und nicht an der
|
||||
* Physik. Beim Auto aber nicht als "kWh je Liter": diese Zahl steht auf keinem
|
||||
* Datenblatt. Eingetragen werden die beiden Verbraeuche, die man kennt, und
|
||||
* die Statistik rechnet daraus, wieviele Liter eine geladene kWh ersetzt.
|
||||
*
|
||||
* Kommt spaeter eine weitere Tabelle desselben Zuschnitts dazu, reicht hier
|
||||
* ein Eintrag - Endpunkt, Maske und Speicherlogik bleiben unveraendert.
|
||||
*/
|
||||
function preisReihen()
|
||||
{
|
||||
return [
|
||||
"strom" => [
|
||||
"titel" => "Strompreise",
|
||||
"hinweis" => "Arbeitspreis, Einspeisevergütung und Grundpreis, wie sie in die "
|
||||
. "Jahresstatistik auf der Historienseite eingehen.",
|
||||
"tabelle" => "gridCosts",
|
||||
"symbol" => "bi-plug",
|
||||
"felder" => [
|
||||
"cost" => ["titel" => "Arbeitspreis", "einheit" => "ct/kWh",
|
||||
"faktor" => 100, "stellen" => 2, "max" => 500],
|
||||
"gain" => ["titel" => "Einspeisung", "einheit" => "ct/kWh",
|
||||
"faktor" => 100, "stellen" => 2, "max" => 500],
|
||||
"base" => ["titel" => "Grundpreis", "einheit" => "€/Jahr",
|
||||
"faktor" => 1, "stellen" => 2, "max" => 5000],
|
||||
],
|
||||
],
|
||||
"gas" => [
|
||||
"titel" => "Gaspreise",
|
||||
"hinweis" => "Womit die Ersparnis der Heizung gerechnet wird: was das Gas kostet, "
|
||||
. "das der Heizstab bei Sonnenüberschuss ersetzt.",
|
||||
"tabelle" => "gasCosts",
|
||||
"symbol" => "bi-fire",
|
||||
"felder" => [
|
||||
"cost" => ["titel" => "Gaspreis", "einheit" => "ct/l",
|
||||
"faktor" => 100, "stellen" => 2, "max" => 1000],
|
||||
"kwhPerLitre" => ["titel" => "Energieinhalt", "einheit" => "kWh/l",
|
||||
"faktor" => 1, "stellen" => 2, "max" => 20],
|
||||
],
|
||||
],
|
||||
"sprit" => [
|
||||
"titel" => "Benzinpreise",
|
||||
"hinweis" => "Für „Benzin gespart“ und „Ersparnis Benzin“: der Preis an der Säule "
|
||||
. "und was die beiden Autos verbrauchen — daraus ergibt sich, wieviele "
|
||||
. "Liter eine geladene Kilowattstunde ersetzt.",
|
||||
"tabelle" => "fuelCosts",
|
||||
"symbol" => "bi-fuel-pump",
|
||||
"felder" => [
|
||||
"cost" => ["titel" => "Benzinpreis", "einheit" => "€/l",
|
||||
"faktor" => 1, "stellen" => 3, "max" => 10],
|
||||
"lPer100km" => ["titel" => "Verbrenner", "einheit" => "l/100 km",
|
||||
"faktor" => 1, "stellen" => 1, "max" => 50],
|
||||
"kwhPer100km" => ["titel" => "Elektro", "einheit" => "kWh/100 km",
|
||||
"faktor" => 1, "stellen" => 1, "max" => 100],
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/** Beschreibung einer Reihe, oder null bei unbekanntem Schluessel. */
|
||||
function preisReihe($key)
|
||||
{
|
||||
$reihen = preisReihen();
|
||||
return isset($reihen[$key]) ? $reihen[$key] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Spaltenbreiten einer Zeile im Bootstrap-Raster (zusammen zwoelf).
|
||||
*
|
||||
* Damit Kopfzeile und Felder aus derselben Quelle kommen: das PHP zeichnet die
|
||||
* Ueberschriften, das JavaScript die Zeilen, und beide holen sich die Breiten
|
||||
* hier.
|
||||
*
|
||||
* Datum, Ende und Papierkorb nehmen sich so wenig wie moeglich - das Ende ist
|
||||
* nur noch Text, der Papierkorb ein Knopf. Was uebrig bleibt, teilen sich die
|
||||
* Felder. Geht die Teilung nicht auf, bekommt der Rest das Feld mit der
|
||||
* laengsten Einheit: hinter "kWh/100 km" bleiben von einer Sechstelspalte
|
||||
* keine 50 Pixel fuer die Zahl, hinter "EUR/l" ist reichlich Platz.
|
||||
*
|
||||
* Ab vier Feldern wird es eng; die Zeile bricht dann um, was haesslicher, aber
|
||||
* nicht kaputt ist.
|
||||
*/
|
||||
function preisSpalten($felder)
|
||||
{
|
||||
$felder = array_values($felder);
|
||||
$anzahl = max(1, count($felder));
|
||||
$rand = $anzahl >= 3 ? ["ab" => 2, "bis" => 2, "aktion" => 1]
|
||||
: ["ab" => 3, "bis" => 3, "aktion" => 1];
|
||||
$rest = 12 - $rand["ab"] - $rand["bis"] - $rand["aktion"];
|
||||
|
||||
$breiten = array_fill(0, $anzahl, max(1, intdiv($rest, $anzahl)));
|
||||
$uebrig = $rest - array_sum($breiten);
|
||||
if ($uebrig > 0) {
|
||||
$laengen = array_map(function ($f) { return mb_strlen($f["einheit"]); }, $felder);
|
||||
$breiten[array_search(max($laengen), $laengen)] += $uebrig;
|
||||
}
|
||||
return $rand + ["felder" => $breiten];
|
||||
}
|
||||
|
||||
/**
|
||||
* Alle Zeitraeume einer Reihe, aeltester zuerst, Werte in Anzeigeeinheit.
|
||||
*
|
||||
* Geliefert wird nur der Stichtag; wo eine Zeile endet, ergibt sich aus der
|
||||
* naechsten und rechnet die Maske selbst aus.
|
||||
*/
|
||||
function preiseLaden($key)
|
||||
{
|
||||
$reihe = preisReihe($key);
|
||||
if (!$reihe) {
|
||||
throw new InvalidArgumentException("Unbekannte Preistabelle.");
|
||||
}
|
||||
$spalten = array_keys($reihe["felder"]);
|
||||
$res = solarDb()->query("SELECT active_date, " . implode(", ", $spalten)
|
||||
. " FROM " . $reihe["tabelle"] . " ORDER BY active_date");
|
||||
if (!$res) {
|
||||
throw new RuntimeException(solarDb()->error);
|
||||
}
|
||||
$zeilen = [];
|
||||
while ($row = $res->fetch_assoc()) {
|
||||
$zeile = ["ab" => $row["active_date"]];
|
||||
foreach ($spalten as $spalte) {
|
||||
$feld = $reihe["felder"][$spalte];
|
||||
$zeile[$spalte] = round(floatval($row[$spalte]) * $feld["faktor"], $feld["stellen"]);
|
||||
}
|
||||
$zeilen[] = $zeile;
|
||||
}
|
||||
return $zeilen;
|
||||
}
|
||||
|
||||
/** Datum in der Form JJJJ-MM-TT, sonst null. */
|
||||
function preisDatum($wert)
|
||||
{
|
||||
if (!is_string($wert) || !preg_match('/^\d{4}-\d{2}-\d{2}$/', $wert)) {
|
||||
return null;
|
||||
}
|
||||
$datum = DateTimeImmutable::createFromFormat("!Y-m-d", $wert);
|
||||
// createFromFormat schluckt den 31. Februar und macht den 3. Maerz daraus;
|
||||
// der Rueckvergleich faengt das ab.
|
||||
return ($datum && $datum->format("Y-m-d") === $wert) ? $datum : null;
|
||||
}
|
||||
|
||||
/** Tag der aeltesten Messung in EnergyFlow, als "JJJJ-MM-TT". */
|
||||
function ersteMessung()
|
||||
{
|
||||
static $tag = null;
|
||||
if ($tag === null) {
|
||||
$res = solarDb()->query("SELECT MIN(datetime) FROM EnergyFlow");
|
||||
$zeile = $res ? $res->fetch_row() : null;
|
||||
$tag = ($zeile && $zeile[0]) ? substr($zeile[0], 0, 10) : "";
|
||||
}
|
||||
return $tag;
|
||||
}
|
||||
|
||||
/**
|
||||
* Beginnt die Reihe zu spaet? Dann fehlt den aeltesten Messwerten der Preis.
|
||||
*
|
||||
* Das ist die einzige Luecke, die es noch geben kann - alle anderen hat das
|
||||
* Weglassen der Spalte end_date erledigt. Auftreten kann sie, wenn jemand den
|
||||
* aeltesten Zeitraum loescht.
|
||||
*/
|
||||
function preisLuecke($zeilen)
|
||||
{
|
||||
$erste = ersteMessung();
|
||||
if (!$erste || !$zeilen) {
|
||||
return "";
|
||||
}
|
||||
$stichtag = $zeilen[0]["ab"];
|
||||
return $stichtag > $erste ? $erste : "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Eine ganze Reihe schreiben: pruefen und ersetzen.
|
||||
*
|
||||
* Ersetzt statt abzugleichen. Die Tabellen haben eine Handvoll Zeilen und
|
||||
* keinen kuenstlichen Schluessel - der Stichtag selbst ist der
|
||||
* Primaerschluessel, und beim Verschieben eines Stichtags waere ein Abgleich
|
||||
* mehr Buchhaltung als Gewinn. Alles laeuft in einer Transaktion, es gibt also
|
||||
* keinen Moment, in dem die Statistik auf eine halbe Tabelle blickt.
|
||||
*/
|
||||
function preiseSpeichern($key, $zeilen)
|
||||
{
|
||||
$reihe = preisReihe($key);
|
||||
if (!$reihe) {
|
||||
throw new InvalidArgumentException("Unbekannte Preistabelle.");
|
||||
}
|
||||
if (!is_array($zeilen) || !$zeilen) {
|
||||
throw new InvalidArgumentException("Es muss mindestens ein Zeitraum bleiben.");
|
||||
}
|
||||
$spalten = array_keys($reihe["felder"]);
|
||||
|
||||
// --- Pruefen --------------------------------------------------------
|
||||
$geprueft = [];
|
||||
foreach ($zeilen as $zeile) {
|
||||
$ab = preisDatum($zeile["ab"] ?? "");
|
||||
if (!$ab) {
|
||||
throw new InvalidArgumentException("Ein Zeitraum hat kein gültiges Startdatum.");
|
||||
}
|
||||
$satz = ["ab" => $ab];
|
||||
foreach ($spalten as $spalte) {
|
||||
$feld = $reihe["felder"][$spalte];
|
||||
$wert = $zeile[$spalte] ?? null;
|
||||
if (!is_numeric($wert)) {
|
||||
throw new InvalidArgumentException(
|
||||
$feld["titel"] . " ab " . $ab->format("d.m.Y") . " ist keine Zahl.");
|
||||
}
|
||||
$wert = floatval($wert);
|
||||
if ($wert < 0 || $wert > $feld["max"]) {
|
||||
throw new InvalidArgumentException(
|
||||
$feld["titel"] . " ab " . $ab->format("d.m.Y")
|
||||
. " liegt außerhalb des Erwartbaren (0 bis " . $feld["max"] . " "
|
||||
. $feld["einheit"] . ").");
|
||||
}
|
||||
$satz[$spalte] = $wert / $feld["faktor"];
|
||||
}
|
||||
$geprueft[] = $satz;
|
||||
}
|
||||
|
||||
// Nach Stichtag sortieren; zwei gleiche Stichtage kann die Tabelle nicht
|
||||
// halten (Primaerschluessel) und die Statistik nicht deuten.
|
||||
usort($geprueft, function ($a, $b) { return $a["ab"] <=> $b["ab"]; });
|
||||
for ($i = 1; $i < count($geprueft); $i++) {
|
||||
if ($geprueft[$i]["ab"] == $geprueft[$i - 1]["ab"]) {
|
||||
throw new InvalidArgumentException(
|
||||
"Zwei Zeiträume beginnen am " . $geprueft[$i]["ab"]->format("d.m.Y") . ".");
|
||||
}
|
||||
}
|
||||
|
||||
// --- Schreiben ------------------------------------------------------
|
||||
$db = solarDb();
|
||||
$db->begin_transaction();
|
||||
try {
|
||||
if (!$db->query("DELETE FROM " . $reihe["tabelle"])) {
|
||||
throw new RuntimeException($db->error);
|
||||
}
|
||||
$platzhalter = implode(", ", array_fill(0, count($spalten) + 1, "?"));
|
||||
$stmt = $db->prepare("INSERT INTO " . $reihe["tabelle"]
|
||||
. " (active_date, " . implode(", ", $spalten) . ")"
|
||||
. " VALUES (" . $platzhalter . ")");
|
||||
if (!$stmt) {
|
||||
throw new RuntimeException($db->error);
|
||||
}
|
||||
foreach ($geprueft as $satz) {
|
||||
$werte = [$satz["ab"]->format("Y-m-d")];
|
||||
foreach ($spalten as $spalte) {
|
||||
$werte[] = $satz[$spalte];
|
||||
}
|
||||
$stmt->bind_param("s" . str_repeat("d", count($spalten)), ...$werte);
|
||||
if (!$stmt->execute()) {
|
||||
throw new RuntimeException($stmt->error);
|
||||
}
|
||||
}
|
||||
$db->commit();
|
||||
} catch (Throwable $e) {
|
||||
$db->rollback();
|
||||
throw $e;
|
||||
}
|
||||
return count($geprueft);
|
||||
}
|
||||
|
||||
/**
|
||||
* Grundpreis Strom fuer ein Kalenderjahr, in Euro.
|
||||
*
|
||||
* Der Grundpreis ist der einzige Posten, der nicht an einem Messwert haengt -
|
||||
* er faellt auch an, wenn nichts verbraucht wird. Deshalb steht er nicht im
|
||||
* grossen SUM-Ausdruck der Statistik, sondern wird hier eigens gerechnet:
|
||||
* jeder Zeitraum steuert seinen Anteil an den Tagen des Jahres bei. Das ist
|
||||
* unabhaengig davon, wieviele Messwerte tatsaechlich aufgezeichnet wurden -
|
||||
* 2022 hat merklich Luecken, und ein fehlender Nachmittag darf den Grundpreis
|
||||
* nicht senken.
|
||||
*
|
||||
* LEAD() holt den naechsten Stichtag; einen Tag davor endet der Zeitraum. Die
|
||||
* juengste Zeile hat keinen Nachfolger und laeuft bis zum Jahresende weiter.
|
||||
*/
|
||||
function grundpreisImJahr($jahr)
|
||||
{
|
||||
$jahr = intval($jahr);
|
||||
$von = $jahr . "-01-01";
|
||||
$bis = $jahr . "-12-31";
|
||||
$tage = intval((new DateTimeImmutable($bis))->format("z")) + 1; // 365 oder 366
|
||||
|
||||
$stmt = solarDb()->prepare(
|
||||
"WITH p AS (SELECT active_date, base,
|
||||
LEAD(active_date) OVER (ORDER BY active_date) AS folgt
|
||||
FROM gridCosts)
|
||||
SELECT SUM(base * (DATEDIFF(LEAST(IFNULL(DATE_SUB(folgt, INTERVAL 1 DAY), ?), ?),
|
||||
GREATEST(active_date, ?)) + 1) / ?)
|
||||
FROM p
|
||||
WHERE active_date <= ? AND (folgt IS NULL OR folgt > ?)");
|
||||
$stmt->bind_param("sssiss", $bis, $bis, $von, $tage, $bis, $von);
|
||||
$stmt->execute();
|
||||
$wert = $stmt->get_result()->fetch_row()[0];
|
||||
return $wert === null ? 0.0 : floatval($wert);
|
||||
}
|
||||
@@ -62,6 +62,11 @@
|
||||
<?php if ($page["charts"]): ?>
|
||||
<?php include __DIR__ . "/charts.html"; ?>
|
||||
<?php endif; ?>
|
||||
<?php if ($page["leaflet"]): ?>
|
||||
<!-- Leaflet 1.9.4, BSD 2-Clause. Liegt wie alle anderen Bibliotheken lokal;
|
||||
die Kacheln kommen ueber ajax/tile.php aus dem eigenen Zwischenspeicher. -->
|
||||
<script src="js/leaflet.js"></script>
|
||||
<?php endif; ?>
|
||||
<script src="js/solar/common.js?v=<?= filemtime("js/solar/common.js") ?>"></script>
|
||||
<?php foreach ($page["scripts"] as $script): ?>
|
||||
<script src="<?= $script ?>?v=<?= filemtime($script) ?>"></script>
|
||||
|
||||
@@ -44,6 +44,12 @@ if (!isset($_GET["floor"]))
|
||||
<link rel="stylesheet" href="./css/adminlte.min.css?v=2" />
|
||||
<!--end::Required Plugin(AdminLTE)-->
|
||||
|
||||
<!--begin::Karte (Leaflet) - nur auf Seiten mit Karte-->
|
||||
<?php if ($page["leaflet"]): ?>
|
||||
<link rel="stylesheet" href="css/leaflet.css" />
|
||||
<?php endif; ?>
|
||||
<!--end::Karte-->
|
||||
|
||||
|
||||
</head>
|
||||
<!--end::Head-->
|
||||
@@ -142,6 +148,12 @@ if (!isset($_GET["floor"]))
|
||||
<p>Wetter</p>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="?action=skoda" class="nav-link <?php if($_GET["action"]=="skoda"){echo " active";} ?>">
|
||||
<i class="nav-icon bi bi-ev-front"></i>
|
||||
<p>Škoda</p>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="nav-item menu-open">
|
||||
@@ -170,6 +182,14 @@ if (!isset($_GET["floor"]))
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<!-- Einstellungen stehen fuer sich: kein Dashboard,
|
||||
sondern die Parameter dahinter. -->
|
||||
<li class="nav-item">
|
||||
<a href="?action=settings" class="nav-link<?php if($_GET["action"]=="settings"){echo " active";} ?>">
|
||||
<i class="nav-icon bi bi-gear"></i>
|
||||
<p>Einstellungen</p>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<!--end::Sidebar Menu-->
|
||||
</nav>
|
||||
|
||||
@@ -74,7 +74,7 @@ $statistics = [
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row-cols-1 row-cols-md-1 g-4 p-2 pb-0" id="<?= $id ?>">
|
||||
<div class="row-cols-1 row-cols-md-1 g-4 p-2" id="<?= $id ?>">
|
||||
</div>
|
||||
</div>
|
||||
<!-- /.card -->
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
<?php
|
||||
/**
|
||||
* Einstellungsseite: Parameter, die von Hand gepflegt werden.
|
||||
*
|
||||
* Bisher sind das die Preistabellen aus restricted/costs.php. Die Seite ist
|
||||
* als Sammlung von Karten angelegt - eine Karte je Thema. Kommt spaeter ein
|
||||
* weiterer Parameter dazu, kommt er als eigene Karte hinzu, ohne dass die
|
||||
* vorhandenen etwas davon merken.
|
||||
*
|
||||
* Die Zeilen zeichnet js/solar/settings.js aus dem Modell, das hier
|
||||
* eingebettet wird - dieselbe Arbeitsteilung wie im Automatik-Editor. Sonst
|
||||
* gaebe es die erste Zeile als PHP und jede weitere als JavaScript, und die
|
||||
* beiden Fassungen liefen frueher oder spaeter auseinander. Aus demselben
|
||||
* Grund kommen die Spaltenbreiten aus preisSpalten(): die Kopfzeile hier und
|
||||
* die Felder dort muessen dieselben sein.
|
||||
*/
|
||||
require_once(__DIR__ . "/costs.php");
|
||||
|
||||
$reihen = preisReihen();
|
||||
$daten = [];
|
||||
$fehler = "";
|
||||
try {
|
||||
foreach ($reihen as $key => $reihe) {
|
||||
$daten[$key] = [
|
||||
"felder" => $reihe["felder"],
|
||||
"spalten" => preisSpalten($reihe["felder"]),
|
||||
"zeilen" => preiseLaden($key),
|
||||
];
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
$fehler = $e->getMessage();
|
||||
}
|
||||
?>
|
||||
<!--begin::App Main-->
|
||||
<main class="app-main">
|
||||
<!--begin::App Content Header-->
|
||||
<div class="app-content-header">
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<h3 class="mb-0">Einstellungen</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--end::App Content Header-->
|
||||
<!--begin::App Content-->
|
||||
<div class="app-content">
|
||||
<div class="container-fluid">
|
||||
<?php if ($fehler): ?>
|
||||
<div class="alert alert-danger">Die Einstellungen sind nicht erreichbar: <?= htmlspecialchars($fehler) ?></div>
|
||||
<?php endif; ?>
|
||||
<?php foreach ($reihen as $key => $reihe): ?>
|
||||
<?php $breiten = preisSpalten($reihe["felder"]); ?>
|
||||
<div class="row">
|
||||
<div class="col-12 col-xxl-9">
|
||||
<div class="card mb-4 preis-karte" data-reihe="<?= htmlspecialchars($key) ?>">
|
||||
<div class="card-header pb-2 pt-2">
|
||||
<h3 class="card-title"><i class="bi <?= $reihe["symbol"] ?> me-2"></i><?= htmlspecialchars($reihe["titel"]) ?></h3>
|
||||
<div class="card-tools">
|
||||
<button type="button" class="btn btn-tool preis-neu" title="Zeitraum hinzufügen">
|
||||
<i class="bi bi-plus-lg"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-tool preis-zurueck" title="Änderungen verwerfen">
|
||||
<i class="bi bi-arrow-counterclockwise"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body pb-2">
|
||||
<p class="text-secondary small mb-3"><?= htmlspecialchars($reihe["hinweis"]) ?></p>
|
||||
|
||||
<!-- Spaltenköpfe nur auf breiten Schirmen; auf dem Handy trägt
|
||||
jedes Feld seine eigene Beschriftung. -->
|
||||
<div class="row g-2 d-none d-xl-flex preis-kopf text-secondary small fw-semibold">
|
||||
<div class="col-xl-<?= $breiten["ab"] ?>">Gültig ab</div>
|
||||
<div class="col-xl-<?= $breiten["bis"] ?>">Gültig bis</div>
|
||||
<?php foreach (array_values($reihe["felder"]) as $i => $feld): ?>
|
||||
<div class="col-xl-<?= $breiten["felder"][$i] ?>"><?= htmlspecialchars($feld["titel"]) ?></div>
|
||||
<?php endforeach; ?>
|
||||
<div class="col-xl-<?= $breiten["aktion"] ?>"></div>
|
||||
</div>
|
||||
|
||||
<div class="preis-zeilen"></div>
|
||||
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary mt-3 preis-neu">
|
||||
<i class="bi bi-plus-lg me-1"></i>Zeitraum hinzufügen
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-footer d-flex align-items-center gap-3">
|
||||
<button type="button" class="btn btn-primary preis-speichern">Speichern</button>
|
||||
<span class="preis-hinweis small"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
<!--end::App Content-->
|
||||
</main>
|
||||
<!--end::App Main-->
|
||||
<script>
|
||||
// Ausgangsstand der Preistabellen. Werte in der Anzeigeeinheit (ct statt
|
||||
// Euro); umgerechnet wird in restricted/costs.php - an genau einer Stelle.
|
||||
const preisDaten = <?= json_encode($daten, JSON_UNESCAPED_UNICODE) ?>;
|
||||
// Aelteste Messung in EnergyFlow. Beginnt eine Preisreihe spaeter, fehlt
|
||||
// den Werten davor der Preis - dann warnt die Maske.
|
||||
const ersteMessung = <?= json_encode(ersteMessung()) ?>;
|
||||
</script>
|
||||
@@ -0,0 +1,383 @@
|
||||
<?php
|
||||
/*
|
||||
* Škoda-Dashboard.
|
||||
*
|
||||
* Zwei Datenwege, bewusst getrennt:
|
||||
*
|
||||
* MQTT Ladestand, Verriegelung, Reichweite, Ladezustand und
|
||||
* Wallbox-Leistung kommen laufend über solarManager/# und
|
||||
* aktualisieren die Anzeige sofort.
|
||||
*
|
||||
* ajax/skoda.php Alles, was nur in der Tabelle steht - Türen, Fenster,
|
||||
* Licht, Kilometerstand, Position, Klima - sowie sämtliche
|
||||
* Auswertungen. Wird beim Laden und danach minütlich geholt.
|
||||
*
|
||||
* Solange kein Fahrzeug angebunden ist, bleibt die Tabelle leer; jeder
|
||||
* Abschnitt zeigt dann seinen Leerzustand statt eines Fehlers.
|
||||
*
|
||||
* Die Fahrzeuggrafik ist eine maßstäbliche Draufsicht des Enyaq:
|
||||
* 4648 x 1877 mm, Radstand 2765 mm, daraus das Seitenverhältnis 2,48:1 und
|
||||
* die Lage der Achsen. Kein Foto, sondern ein Schema - die Teile sollen
|
||||
* ihren Zustand zeigen, nicht das Auto porträtieren.
|
||||
*/
|
||||
?>
|
||||
<!--begin::App Main-->
|
||||
<main class="app-main">
|
||||
<div class="app-content-header">
|
||||
<div class="container-fluid">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-sm-8">
|
||||
<h3 class="mb-0">Škoda</h3>
|
||||
</div>
|
||||
<div class="col-sm-4 text-sm-end">
|
||||
<span class="skoda-alter text-secondary" id="skodaAlter"> </span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="app-content">
|
||||
<div class="container-fluid">
|
||||
|
||||
<!-- ================================================ Zustand ===== -->
|
||||
<div class="row">
|
||||
|
||||
<!-- Das Fahrzeug als Anzeigeelement. Jedes Teil trägt seinen Zustand
|
||||
selbst: offene Türen und Klappen leuchten bernstein, Licht gelb,
|
||||
der Ladeanschluss pulst beim Laden. Die Zeile darunter benennt
|
||||
dasselbe in Worten und hebt beim Überfahren das zugehörige Teil
|
||||
hervor - Farbe allein soll die Aussage nicht tragen. -->
|
||||
<div class="col-xxl-4 col-lg-5 d-flex align-items-stretch">
|
||||
<div class="card mb-4 p-0 w-100">
|
||||
<div class="card-header pb-0 pt-1">
|
||||
<h3 class="card-title">Fahrzeug</h3>
|
||||
<div class="card-tools">
|
||||
<span class="badge text-bg-secondary" id="skodaSchloss">–</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body d-flex flex-column align-items-center pt-2">
|
||||
|
||||
<svg id="skodaAuto" class="skoda-auto" viewBox="0 0 224 500"
|
||||
role="img" aria-label="Draufsicht des Fahrzeugs mit dem Zustand von Türen, Fenstern, Klappen und Licht">
|
||||
|
||||
<!-- Räder. Vorderachse bei 860 mm, Hinterachse bei 3625 mm
|
||||
ab Bugspitze - der Radstand von 2765 mm maßstäblich. -->
|
||||
<g class="rad">
|
||||
<rect x="5" y="68" width="21" height="58" rx="9"/>
|
||||
<rect x="198" y="68" width="21" height="58" rx="9"/>
|
||||
<rect x="5" y="362" width="21" height="58" rx="9"/>
|
||||
<rect x="198" y="362" width="21" height="58" rx="9"/>
|
||||
</g>
|
||||
|
||||
<!-- Karosserie -->
|
||||
<path class="karosserie" d="M112 6
|
||||
C150 6, 198 16, 204 50 C209 90, 212 160, 212 250
|
||||
C212 340, 209 410, 204 450 C198 484, 150 494, 112 494
|
||||
C74 494, 26 484, 20 450 C15 410, 12 340, 12 250
|
||||
C12 160, 15 90, 20 50 C26 16, 74 6, 112 6 Z"/>
|
||||
|
||||
<!-- Tech-Deck Face: die glänzend schwarze Blende über die
|
||||
volle Bugbreite, die den Enyaq seit der Überarbeitung
|
||||
kennzeichnet. Von oben sieht man ihre Oberkante. -->
|
||||
<path class="techdeck" d="M112 7
|
||||
C150 7, 197 17, 203 49 L194 53 C187 27, 148 18, 112 18
|
||||
C76 18, 37 27, 30 53 L21 49 C27 17, 74 7, 112 7 Z"/>
|
||||
|
||||
<!-- Scheinwerfer, vier Elemente je Seite wie beim Original,
|
||||
dazu die Rückleuchten als durchgehendes Band. -->
|
||||
<g id="teil-lights" class="teil teil-licht" data-teil="lights">
|
||||
<rect x="36" y="30" width="9" height="13" rx="4"/>
|
||||
<rect x="48" y="27" width="9" height="13" rx="4"/>
|
||||
<rect x="60" y="25" width="9" height="13" rx="4"/>
|
||||
<rect x="72" y="24" width="9" height="13" rx="4"/>
|
||||
<rect x="143" y="24" width="9" height="13" rx="4"/>
|
||||
<rect x="155" y="25" width="9" height="13" rx="4"/>
|
||||
<rect x="167" y="27" width="9" height="13" rx="4"/>
|
||||
<rect x="179" y="30" width="9" height="13" rx="4"/>
|
||||
<rect class="rueck" x="40" y="462" width="52" height="13" rx="6"/>
|
||||
<rect class="rueck" x="132" y="462" width="52" height="13" rx="6"/>
|
||||
<title>Beleuchtung</title>
|
||||
</g>
|
||||
|
||||
<!-- Motorhaube mit angedeuteter Mittelsicke -->
|
||||
<g id="teil-bonnet" class="teil" tabindex="0" data-teil="bonnet">
|
||||
<path d="M31 152 L193 152 L189 62 C169 40, 55 40, 35 62 Z"/>
|
||||
<title>Motorhaube</title>
|
||||
</g>
|
||||
<path class="sicke" d="M112 60 L112 150"/>
|
||||
|
||||
<!-- Windschutzscheibe, Dach, Heckscheibe -->
|
||||
<path class="scheibe" d="M34 156 L190 156 L178 200 L46 200 Z"/>
|
||||
<rect class="dach" x="46" y="200" width="132" height="136" rx="10"/>
|
||||
|
||||
<!-- Panoramaglasdach: beim Enyaq nahezu die ganze Dachfläche -->
|
||||
<g id="teil-sunroof" class="teil" tabindex="0" data-teil="sunroof">
|
||||
<rect x="62" y="206" width="100" height="120" rx="8"/>
|
||||
<title>Panoramadach</title>
|
||||
</g>
|
||||
|
||||
<!-- Dachreling -->
|
||||
<g class="reling">
|
||||
<rect x="48" y="204" width="9" height="134" rx="4"/>
|
||||
<rect x="167" y="204" width="9" height="134" rx="4"/>
|
||||
</g>
|
||||
|
||||
<path class="scheibe" d="M46 336 L178 336 L190 382 L34 382 Z"/>
|
||||
<rect class="spoiler" x="52" y="330" width="120" height="9" rx="4"/>
|
||||
|
||||
<!-- Heckklappe -->
|
||||
<g id="teil-trunk" class="teil" tabindex="0" data-teil="trunk">
|
||||
<path d="M34 384 L190 384 L186 458 C168 478, 56 478, 38 458 Z"/>
|
||||
<title>Heckklappe</title>
|
||||
</g>
|
||||
|
||||
<!-- Türen, jeweils mit Seitenfenster darin -->
|
||||
<g id="teil-doors" class="teil" data-teil="doors">
|
||||
<g tabindex="0"><rect x="12" y="202" width="23" height="74" rx="6"/><title>Tür vorn links</title></g>
|
||||
<g tabindex="0"><rect x="12" y="278" width="23" height="66" rx="6"/><title>Tür hinten links</title></g>
|
||||
<g tabindex="0"><rect x="189" y="202" width="23" height="74" rx="6"/><title>Tür vorn rechts</title></g>
|
||||
<g tabindex="0"><rect x="189" y="278" width="23" height="66" rx="6"/><title>Tür hinten rechts</title></g>
|
||||
</g>
|
||||
<g id="teil-windows" class="teil teil-fenster" data-teil="windows">
|
||||
<rect x="17" y="210" width="13" height="58" rx="4"/>
|
||||
<rect x="17" y="286" width="13" height="50" rx="4"/>
|
||||
<rect x="194" y="210" width="13" height="58" rx="4"/>
|
||||
<rect x="194" y="286" width="13" height="50" rx="4"/>
|
||||
<title>Seitenfenster</title>
|
||||
</g>
|
||||
|
||||
<!-- Außenspiegel -->
|
||||
<g class="spiegel">
|
||||
<rect x="1" y="194" width="16" height="10" rx="5"/>
|
||||
<rect x="207" y="194" width="16" height="10" rx="5"/>
|
||||
</g>
|
||||
|
||||
<!-- ============================================================
|
||||
Steuerung: die beiden anklickbaren Stellen.
|
||||
|
||||
Bedient wird das, worum es geht - der Ladeanschluss für
|
||||
das Laden, der Innenraum für Klima und Lüftung. Beide
|
||||
tragen einen Ring als Hinweis darauf, dass sie mehr sind
|
||||
als Anzeige, und öffnen ein Modal mit den Befehlen.
|
||||
|
||||
Sie stehen bewusst NICHT in der Zustandszeile darunter:
|
||||
die zeigt, was ist. Diese beiden ändern etwas.
|
||||
============================================================ -->
|
||||
|
||||
<!-- Ladeanschluss. Beim Enyaq sitzt die Klappe am hinteren
|
||||
rechten Kotflügel; sie pulst, solange geladen wird. -->
|
||||
<g id="teil-plug" class="teil teil-stecker steuer" tabindex="0"
|
||||
role="button" data-steuer="laden" aria-label="Laden steuern">
|
||||
<circle class="ring" cx="203" cy="408" r="17"/>
|
||||
<circle cx="203" cy="408" r="11"/>
|
||||
<path d="M198 402 l6 5 -4 0 4 6"/>
|
||||
<title>Laden steuern</title>
|
||||
</g>
|
||||
|
||||
<!-- Innenraum, auf der Windschutzscheibe: Klima und Lüftung. -->
|
||||
<g id="steuer-klima" class="steuer" tabindex="0"
|
||||
role="button" data-steuer="klima" aria-label="Klima und Lüftung steuern">
|
||||
<circle class="ring" cx="112" cy="178" r="19"/>
|
||||
<circle class="flaeche" cx="112" cy="178" r="13"/>
|
||||
<path class="glyphe" d="M112 170 v10 M108 174 h8 M105 185 q7 -4 14 0"/>
|
||||
<title>Klima und Lüftung steuern</title>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
<div class="skoda-teile" id="skodaTeile"></div>
|
||||
<p class="skoda-hinweis">
|
||||
Ladeanschluss und Innenraum antippen, um zu steuern
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Batterie, Reichweite, Ladung -->
|
||||
<div class="col-xxl-8 col-lg-7 d-flex align-items-stretch">
|
||||
<div class="card mb-4 p-0 w-100">
|
||||
<div class="card-header pb-0 pt-1">
|
||||
<h3 class="card-title">Batterie und Ladung</h3>
|
||||
<div class="card-tools">
|
||||
<span class="badge text-bg-secondary" id="skodaLadeZustand">–</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row align-items-center g-3">
|
||||
|
||||
<!-- Ladestandsring. Der Strich auf dem Ring ist das
|
||||
eingestellte Ladeziel - eine Ladung, die dort endet, ist
|
||||
kein Fehler, sondern Absicht. -->
|
||||
<div class="col-md-5 text-center">
|
||||
<svg class="skoda-ring" viewBox="0 0 200 200" role="img" aria-labelledby="skodaSocText">
|
||||
<circle class="ring-bahn" cx="100" cy="100" r="82"/>
|
||||
<circle class="ring-wert" id="skodaRing" cx="100" cy="100" r="82"/>
|
||||
<line class="ring-ziel" id="skodaZiel" x1="100" y1="8" x2="100" y2="26"/>
|
||||
<text id="skodaSocText" class="ring-zahl" x="100" y="98">–</text>
|
||||
<text class="ring-einheit" x="100" y="126">Prozent</text>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div class="col-md-7">
|
||||
<div class="skoda-werte">
|
||||
<div class="skoda-wert"><span class="k">Reichweite</span><span class="v" id="skodaReichweite">–</span></div>
|
||||
<div class="skoda-wert"><span class="k">Ladeleistung</span><span class="v" id="skodaLeistung">–</span></div>
|
||||
<div class="skoda-wert"><span class="k">Restzeit</span><span class="v" id="skodaRestzeit">–</span></div>
|
||||
<div class="skoda-wert"><span class="k">Ladeziel</span><span class="v" id="skodaZielText">–</span></div>
|
||||
<div class="skoda-wert"><span class="k">Kilometerstand</span><span class="v" id="skodaKm">–</span></div>
|
||||
<div class="skoda-wert"><span class="k">Klima</span><span class="v" id="skodaKlima">–</span></div>
|
||||
</div>
|
||||
|
||||
<!-- Woher der Strom gerade kommt: der Balken teilt die
|
||||
Ladeleistung in den Anteil, den die PV deckt, und den
|
||||
Rest aus dem Netz. -->
|
||||
<div class="skoda-quelle mt-3" id="skodaQuelleBox">
|
||||
<div class="d-flex justify-content-between small text-secondary mb-1">
|
||||
<span>Ladestrom</span><span id="skodaQuelleText">–</span>
|
||||
</div>
|
||||
<div class="skoda-balken">
|
||||
<div class="teil-pv" id="skodaAnteilPV"></div>
|
||||
<div class="teil-netz" id="skodaAnteilNetz"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ================================================= Ladungen ==== -->
|
||||
<div class="row">
|
||||
<div class="col-xl-8 d-flex align-items-stretch">
|
||||
<div class="card mb-4 p-0 w-100">
|
||||
<div class="card-header pb-0 pt-1">
|
||||
<h3 class="card-title">Verlauf der letzten Ladung</h3>
|
||||
<div class="card-tools">
|
||||
<span class="text-secondary small" id="skodaKurveZeit"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body pb-1">
|
||||
<div class="chart-container"><canvas id="skodaKurve"></canvas></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xl-4 d-flex align-items-stretch">
|
||||
<div class="card mb-4 p-0 w-100">
|
||||
<div class="card-header pb-0 pt-1">
|
||||
<h3 class="card-title">Ladungen</h3>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="skoda-liste" id="skodaLadungen"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ============================================== Akkugesundheit == -->
|
||||
<div class="row">
|
||||
<div class="col-xl-8 d-flex align-items-stretch">
|
||||
<div class="card mb-4 p-0 w-100">
|
||||
<div class="card-header pb-0 pt-1">
|
||||
<h3 class="card-title">Akkugesundheit</h3>
|
||||
<div class="card-tools">
|
||||
<span class="text-secondary small">kWh je 100 Ladestandsprozent</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body pb-1">
|
||||
<div class="chart-container"><canvas id="skodaGesundheit"></canvas></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xl-4 d-flex align-items-stretch">
|
||||
<div class="card mb-4 p-0 w-100">
|
||||
<div class="card-header pb-0 pt-1">
|
||||
<h3 class="card-title">Befund</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="skoda-befund" id="skodaBefund">
|
||||
<p class="text-secondary mb-0">Noch keine ausgewerteten Ladungen.</p>
|
||||
</div>
|
||||
<p class="text-secondary small mb-0 mt-3">
|
||||
Gemessen wird am Wallbox-Zähler, die Ladeverluste sind also
|
||||
enthalten. Absolut ist der Wert eine Näherung — aussagekräftig
|
||||
ist sein Verlauf, denn ein alternder Akku bräuchte <em>weniger</em>
|
||||
Kilowattstunden für dieselben hundert Prozentpunkte.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ============================================ Standort ========== -->
|
||||
<!-- Karte auf Leaflet. Die Kacheln kommen nicht unmittelbar von
|
||||
OpenStreetMap, sondern über ajax/tile.php aus dem eigenen
|
||||
Zwischenspeicher auf dem NAS - siehe dort, warum.
|
||||
|
||||
Die Fahrtlinien sind gestrichelt, und zwar mit Absicht: die API
|
||||
liefert nur die letzte bekannte PARKposition, also Start und Ziel.
|
||||
Die tatsächlich gefahrene Strecke kennt niemand, und eine
|
||||
durchgezogene Linie auf einer Straßenkarte würde genau das
|
||||
behaupten. -->
|
||||
<div class="row">
|
||||
<div class="col-xl-7 d-flex align-items-stretch">
|
||||
<div class="card mb-4 p-0 w-100">
|
||||
<div class="card-header pb-0 pt-1">
|
||||
<h3 class="card-title">Standort und Fahrten</h3>
|
||||
<div class="card-tools">
|
||||
<button type="button" id="skodaKarteHeim" class="btn btn-tool"
|
||||
title="Auf den aktuellen Standort zentrieren">
|
||||
<i class="bi bi-geo-alt"></i>
|
||||
</button>
|
||||
<a href="#" id="skodaOsmLink" class="btn btn-tool" target="_blank"
|
||||
rel="noopener noreferrer" title="Auf OpenStreetMap öffnen">
|
||||
<i class="bi bi-box-arrow-up-right"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body p-2">
|
||||
<div id="skodaKarte" class="skoda-karte"></div>
|
||||
<div class="d-flex justify-content-between align-items-center px-2 pt-2 pb-1">
|
||||
<span class="text-secondary small" id="skodaStandort">–</span>
|
||||
<span class="skoda-kartenlegende">
|
||||
<span><i class="pkt jetzt"></i>jetzt</span>
|
||||
<span><i class="pkt halt"></i>Halt</span>
|
||||
<span><i class="lin"></i>Luftlinie</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xl-5 d-flex align-items-stretch">
|
||||
<!-- Die Klasse zeigt-kwh / zeigt-prozent auf dieser Kachel
|
||||
entscheidet, welche Einheit die Zeilen anzeigen. Beide Werte
|
||||
stehen im Markup, umgeschaltet wird allein per CSS - so muss
|
||||
beim Wechsel nichts neu gebaut werden, und die Auswahl
|
||||
übersteht auch die minütliche Auffrischung der Liste. -->
|
||||
<div class="card mb-4 p-0 w-100 zeigt-kwh" id="skodaFahrtenKachel">
|
||||
<div class="card-header pb-0 pt-1">
|
||||
<h3 class="card-title">Fahrten</h3>
|
||||
<div class="card-tools d-flex align-items-center gap-2">
|
||||
<!-- Text kommt aus zeigeVerbrauchsbasis(): der Verbrauch in
|
||||
Kilowattstunden ist nicht gemessen, sondern aus dem
|
||||
Ladestand und der eigenen Ladehistorie gerechnet. Womit
|
||||
genau, steht im Titel dieses Elements. -->
|
||||
<span class="text-secondary small" id="skodaVerbrauchBasis"></span>
|
||||
<span class="skoda-umschalter" id="skodaEinheit" role="group"
|
||||
aria-label="Einheit des Verbrauchs">
|
||||
<button type="button" data-einheit="kwh" aria-pressed="true">kWh</button>
|
||||
<button type="button" data-einheit="prozent" aria-pressed="false">%</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="skoda-liste" id="skodaFahrten"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<!--end::App Main-->
|
||||
@@ -74,7 +74,7 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row-cols-1 row-cols-md-1 g-4 p-2 pb-0" id="Stats-Year">
|
||||
<div class="row-cols-1 row-cols-md-1 g-4 p-2" id="Stats-Year">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user