Solar: neue Energiefluss-Uebersicht, anpassbar je Haus

Das feste realtime.svg ist ersetzt durch einen Katalog
(js/solar/energieflussKatalog.js: jeder Kreis einmal, samt Werten aus MQTT)
und einen Renderer (js/solar/energiefluss.js): Ringe mit Segmenten,
Fluesse als Boegen mit laufenden Punkten, Fuellstandswelle fuer Batterie
und Auto, animierte Symbole, Plaketten, Hochkant-Ansicht auf schmalen
Schirmen. Hierarchie und Lage wie bisher: Quellen -> Wechselrichter ->
Verbrauch -> OG/EG/UG -> Wallboxen, Heizstab.

Einstellungen -> Uebersicht: Kreise ein/aus (Kinder ruecken eine Ebene
hoeher), Namen, Lage per Ziehen in der Vorschau, PV-Flaechen. Gespeichert
werden nur Abweichungen in homeMesh.energiefluss.

Der PV-Lageplan hinter der Sonne liegt unveraendert als assets/img/pvplan.svg
und ist ueber den Umschalter Uebersicht/Anlage erreichbar.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-15 16:42:52 +02:00
co-authored by Claude Opus 5
parent 5ed3a0796e
commit a59277e33e
14 changed files with 1812 additions and 1148 deletions
+105
View File
@@ -0,0 +1,105 @@
<?php
/**
* Einstellungen der Energiefluss-Übersicht (Karte "Realtime" auf der
* Solar-Seite), Tabelle homeMesh.energiefluss - siehe homeMesh_energiefluss.sql.
*
* Welche Kreise es gibt und wie sie gezeichnet werden, weiß allein der
* Katalog in js/solar/energieflussKatalog.js. Hier wird nur gespeichert, was
* ein Haus davon abweichend einstellt. PHP kennt deshalb die Kennungen nicht
* und prüft nur ihre Form - ein neuer Kreis im Katalog braucht hier keine
* Zeile.
*/
require_once(__DIR__ . "/meshdb.php");
/**
* Alle Abweichungen, je Kennung.
*
* Fehlt die Tabelle (Installation ohne homeMesh_energiefluss.sql), kommt ein
* leeres Ergebnis zurück: die Übersicht zeichnet dann den Katalog, statt die
* Solar-Seite mit einem Fehler zu verlieren.
*
* @return array<string, array{aktiv: bool, name: ?string, x: ?int, y: ?int, optionen: ?array}>
*/
function energieflussLaden()
{
$ergebnis = [];
try {
$res = meshDb()->query("SELECT id, aktiv, name, x, y, optionen FROM energiefluss");
} catch (Throwable $e) {
return $ergebnis;
}
while ($res && $z = $res->fetch_assoc()) {
$optionen = $z["optionen"] !== null ? json_decode($z["optionen"], true) : null;
$ergebnis[$z["id"]] = [
"aktiv" => (bool)$z["aktiv"],
"name" => $z["name"],
"x" => $z["x"] !== null ? (int)$z["x"] : null,
"y" => $z["y"] !== null ? (int)$z["y"] : null,
"optionen" => is_array($optionen) ? $optionen : null,
];
}
return $ergebnis;
}
/**
* Den ganzen Stand ersetzen.
*
* Die Maske schickt immer alle Kreise, auch unveränderte - so gibt es keinen
* Zwischenstand, in dem die Hälfte gespeichert ist. Einträge, die dem Katalog
* entsprechen (aktiv, ohne Name, Lage und Optionen), werden gar nicht erst
* geschrieben: die Tabelle hält nur echte Abweichungen.
*
* @param array $knoten [{id, aktiv, name, x, y, optionen}, ...]
* @return int Anzahl gespeicherter Abweichungen
*/
function energieflussSpeichern($knoten)
{
if (!is_array($knoten)) {
throw new InvalidArgumentException("Keine Kreise übergeben.");
}
$zeilen = [];
foreach ($knoten as $k) {
$id = (string)($k["id"] ?? "");
if (!preg_match('/^[A-Za-z][A-Za-z0-9_]{0,19}$/', $id)) {
throw new InvalidArgumentException("Ungültige Kennung: " . $id);
}
$name = trim((string)($k["name"] ?? ""));
if (mb_strlen($name) > 40) {
throw new InvalidArgumentException("Der Name „" . $name . "“ ist länger als 40 Zeichen.");
}
$x = isset($k["x"]) && $k["x"] !== null ? (int)round($k["x"]) : null;
$y = isset($k["y"]) && $k["y"] !== null ? (int)round($k["y"]) : null;
foreach ([$x, $y] as $w) {
if ($w !== null && ($w < -2000 || $w > 4000)) {
throw new InvalidArgumentException("Die Lage von „" . $id . "“ liegt außerhalb der Zeichenfläche.");
}
}
$optionen = $k["optionen"] ?? null;
if ($optionen !== null && !is_array($optionen)) {
throw new InvalidArgumentException("Optionen von „" . $id . "“ sind kein Objekt.");
}
$aktiv = !isset($k["aktiv"]) || (bool)$k["aktiv"];
if ($aktiv && $name === "" && $x === null && $y === null && $optionen === null) {
continue;
}
$zeilen[] = [$id, $aktiv ? 1 : 0, $name === "" ? null : $name, $x, $y,
$optionen === null ? null : json_encode($optionen, JSON_UNESCAPED_UNICODE)];
}
$db = meshDb();
$db->begin_transaction();
try {
$db->query("DELETE FROM energiefluss");
$st = $db->prepare("INSERT INTO energiefluss (id, aktiv, name, x, y, optionen) VALUES (?, ?, ?, ?, ?, ?)");
foreach ($zeilen as $z) {
$st->bind_param("sisiis", $z[0], $z[1], $z[2], $z[3], $z[4], $z[5]);
$st->execute();
}
$db->commit();
} catch (Throwable $e) {
$db->rollback();
throw new RuntimeException("Die Übersicht wurde nicht gespeichert: " . $e->getMessage());
}
return count($zeilen);
}
+43
View File
@@ -33,6 +33,7 @@ require_once(__DIR__ . "/skodaKeys.php");
$reiter = [
["id" => "preise", "titel" => "Preise", "symbol" => "bi-currency-euro"],
["id" => "grundriss", "titel" => "Grundriss", "symbol" => "bi-house"],
["id" => "energiefluss", "titel" => "Übersicht", "symbol" => "bi-diagram-3"],
["id" => "kacheln", "titel" => "Home-Kacheln", "symbol" => "bi-grid-1x2"],
["id" => "geraete", "titel" => "Geräte", "symbol" => "bi-house-gear"],
["id" => "fahrzeug", "titel" => "Fahrzeug", "symbol" => "bi-ev-front"],
@@ -162,6 +163,48 @@ try {
</div>
<?= reiterFlaecheZu() ?><!-- /Grundriss -->
<!-- ================================================== Übersicht ==== -->
<!-- Die Energiefluss-Übersicht der Solar-Seite. Welche Kreise es gibt,
steht im Katalog (js/solar/energieflussKatalog.js); hier wird nur
eingestellt, was dieses Haus davon abweicht - gespeichert in
homeMesh.energiefluss. Vorschau und Liste zeichnet
js/solar/energieflussEinstellungen.js. -->
<?= reiterFlaecheAuf("energiefluss", $reiterOpt) ?>
<div class="card mb-4" id="energieflussKarte">
<div class="card-header pb-2 pt-2">
<h3 class="card-title"><i class="bi bi-diagram-3 me-2"></i>Energiefluss-Übersicht</h3>
<div class="card-tools">
<button type="button" class="btn btn-tool" id="energieflussNeuLaden" title="Änderungen verwerfen">
<i class="bi bi-arrow-counterclockwise"></i>
</button>
</div>
</div>
<div class="card-body">
<p class="text-secondary small mb-3">
Kreise, die es in diesem Haus nicht gibt, werden ausgeschaltet hängen andere
daran, rücken sie eine Ebene nach oben. In der Vorschau lassen sich die Kreise
mit der Maus verschieben. Auf schmalen Bildschirmen dreht die Übersicht sich
von selbst hochkant. Die Werte in der Vorschau sind Beispiele.
</p>
<div class="row g-4">
<div class="col-12 col-xl-7">
<div class="enf-vorschau" id="energieflussVorschau"></div>
</div>
<div class="col-12 col-xl-5">
<div id="energieflussListe">
<p class="text-secondary small mb-0">Wird geladen …</p>
</div>
</div>
</div>
</div>
<div class="card-footer d-flex flex-wrap align-items-center gap-3">
<button type="button" class="btn btn-primary" id="energieflussSpeichern">Speichern</button>
<button type="button" class="btn btn-outline-secondary" id="energieflussZuruecksetzen">Alles auf Katalog zurücksetzen</button>
<span class="small" id="energieflussHinweis"></span>
</div>
</div>
<?= reiterFlaecheZu() ?><!-- /Übersicht -->
<!-- ================================================ Home-Kacheln ==== -->
<!-- Der Inhalt kommt aus js/solar/settings.js: eine Karte je Etage, darin
je Raum eine Zeile mit ihren Werten. Gezeichnet wird aus dem Modell,
+24 -9
View File
@@ -1,3 +1,4 @@
<?php require_once(__DIR__ . "/energiefluss.php"); ?>
<style>
.datepicker-native {
position: relative;
@@ -47,22 +48,36 @@
<!--begin::Row-->
<div class="row">
<!-- Start col -->
<!-- Energiefluss. Gezeichnet von js/solar/energiefluss.js aus dem Katalog
(energieflussKatalog.js) und den Einstellungen dieses Hauses
(Einstellungen -> Übersicht). Bis September 2026 stand hier
assets/img/realtime.svg mit festen Kreisen. -->
<div class="col-xxl-12 col-xl-12 d-flex align-items-stretch">
<div class="card p-0 mb-4" style="width:100%">
<div class="card-header pb-0 pt-1">
<h3 class="card-title">Realtime</h3>
<div class="card p-0 mb-4" style="width:100%" id="energieflussKarte">
<div class="card-header pb-1 pt-1 d-flex align-items-center">
<h3 class="card-title me-auto">Realtime</h3>
<div class="enf-umschalter me-2" role="tablist" aria-label="Ansicht">
<button type="button" class="btn btn-sm active" role="tab" aria-selected="true" data-enf-ansicht="fluss">Übersicht</button>
<button type="button" class="btn btn-sm" role="tab" aria-selected="false" data-enf-ansicht="anlage">Anlage</button>
</div>
<div class="card-tools">
<button type="button" class="btn btn-tool" data-lte-toggle="card-remove">
<i class="bi bi-x-lg"></i>
</button>
<a class="btn btn-tool" href="index.php?action=settings#energiefluss" title="Übersicht einstellen">
<i class="bi bi-sliders"></i>
</a>
</div>
</div>
<div class="card-body p-0 d-flex flex-column" id="realtimeSVGcontainer" style="position:relative;max-height:800px;">
<?php readfile(__DIR__ . "/../assets/img/realtime.svg"); ?>
<div class="card-body p-2 p-md-3">
<div class="enf-ansicht enf-behaelter" data-enf-ansicht-flaeche="fluss" id="energiefluss"></div>
<div class="enf-ansicht" data-enf-ansicht-flaeche="anlage" hidden>
<?php readfile(__DIR__ . "/../assets/img/pvplan.svg"); ?>
</div>
</div>
</div>
<!-- /.card -->
</div>
<script>
// Abweichungen dieses Hauses vom Katalog, siehe restricted/energiefluss.php.
const energieflussEinstellungen = <?= json_encode((object)energieflussLaden(), JSON_UNESCAPED_UNICODE) ?>;
</script>
<div class="col-xxl-12 col-xl-12 col-lg-12 col-md-12 d-flex align-items-stretch">
<div class="card p-0 mb-4" style="width:100%">