Files
adminandClaude Opus 5 a59277e33e 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>
2026-09-15 16:42:52 +02:00

236 lines
9.4 KiB
PHP

<?php
/**
* Endpunkt der Einstellungsseite.
*
* GET ?action=list&reihe=strom Zeitraeume einer Preistabelle
* POST ?action=save {"reihe": "strom", "zeilen": [ ... ]}
* GET ?action=kacheln Anzeigewerte aller Home-Kacheln
* POST ?action=kacheln-save {"floor": "OG", "room": "bad",
* "werte": [ ... ] oder null}
* GET ?action=geraete Geraete mit Raum, Verwendung und Alter
* POST ?action=geraete-raeume {"rooms": {"123": "11", ...}} Geraet -> Raumnummer
* 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"]}
*
* GET ?action=grundriss Etagen, Raeume, Kachelpositionen
* POST ?action=etage-speichern {"code": "EG", "label": "...", "standard": true, "neu": false}
* POST ?action=etagen-reihenfolge {"codes": ["OG", "EG", ...]}
* POST ?action=etage-loeschen {"code": "EG"}
* POST ?action=etage-bild multipart: code, bild (Datei)
* POST ?action=raum-speichern {"nr": 0|12, "floor": "EG", "name": "...", "kuerzel": "", "thermostat": ""}
* POST ?action=raum-position {"nr": 12, "x": 120, "y": 80} (x/y null = vom Plan)
* POST ?action=raum-loeschen {"nr": 12}
* Die Grundriss-Aktionen antworten mit dem ganzen neuen Stand.
*
* GET ?action=energiefluss Abweichungen der Übersicht vom Katalog
* POST ?action=energiefluss-save {"knoten": [{id, aktiv, name, x, y, optionen}, ...]}
*
* Ein reiner Verteiler: geprueft und geschrieben wird in
* restricted/costs.php, kacheln.php bzw. grundriss.php, damit Maske und
* Endpunkt dieselbe Wahrheit ueber einen Zeitraum oder eine Kachel haben.
*/
require_once("../helper.php");
require_once("../restricted/costs.php");
require_once("../restricted/kacheln.php");
require_once("../restricted/automations.php");
require_once("../restricted/skodaKeys.php");
require_once("../restricted/grundriss.php");
require_once("../restricted/energiefluss.php");
if (!checkLogin()) {
http_response_code(403);
exit;
}
function jsonAntwort($data, $code = 200)
{
http_response_code($code);
header("Content-Type: application/json; charset=utf-8");
echo json_encode($data, JSON_UNESCAPED_UNICODE);
exit;
}
$action = $_GET["action"] ?? "list";
// Das Grundrissbild kommt als Formular mit Datei, nicht als JSON - deshalb
// vor dem JSON-Teil.
if ($_SERVER["REQUEST_METHOD"] === "POST" && $action === "etage-bild") {
try {
$bild = etageBildSpeichern($_POST["code"] ?? "", $_FILES["bild"] ?? null);
grundriss(true);
jsonAntwort(["bild" => $bild, "stand" => grundrissStand()]);
} catch (InvalidArgumentException $e) {
jsonAntwort(["error" => $e->getMessage()], 400);
} catch (Throwable $e) {
jsonAntwort(["error" => $e->getMessage()], 500);
}
}
if ($_SERVER["REQUEST_METHOD"] === "POST") {
$body = json_decode(file_get_contents("php://input"), true);
if (!is_array($body)) {
jsonAntwort(["error" => "Konnte die Anfrage nicht lesen."], 400);
}
try {
switch ($action) {
case "save":
$anzahl = preiseSpeichern($body["reihe"] ?? "", $body["zeilen"] ?? []);
// Die gespeicherte Fassung zurueckgeben: die Maske zeigt danach
// die abgeleiteten Enddaten, nicht die eingetippten.
jsonAntwort([
"gespeichert" => $anzahl,
"zeilen" => preiseLaden($body["reihe"]),
]);
case "kacheln-save":
/*
* Fehlt "werte" oder steht dort null, faellt die Kachel auf
* ihre Vorgabe zurueck - dann kommt diese zurueck, damit die
* Maske zeigt, was jetzt gilt, statt zu leeren Zeilen.
*/
$floor = $body["floor"] ?? "";
$room = $body["room"] ?? "";
$werte = array_key_exists("werte", $body) ? $body["werte"] : null;
$gespeichert = kachelWerteSpeichern($floor, $room, $werte);
$vorgabe = [];
foreach (allRooms() as $r) {
if ($r["floor"] === $floor && $r["id"] === $room) {
$vorgabe = kachelVorgabe($r);
}
}
jsonAntwort([
"eigen" => $gespeichert !== null,
"werte" => $gespeichert !== null ? $gespeichert : $vorgabe,
]);
case "geraete-raeume":
jsonAntwort(["gespeichert" => saveRooms($body["rooms"] ?? [])]);
case "geraet-loeschen":
/*
* Loeschen ist endgueltig - abgelehnt wird es, wenn eine
* Automatik auf das Geraet zeigt (siehe geraetLoeschen()).
* Zurueck kommt die neue Liste, damit die Maske nicht raten
* muss, was jetzt gilt.
*/
$name = geraetLoeschen($body["id"] ?? 0);
jsonAntwort(["geloescht" => $name, "geraete" => geraeteListe()]);
case "etage-speichern":
etageSpeichern($body);
grundriss(true);
jsonAntwort(["stand" => grundrissStand()]);
case "etagen-reihenfolge":
etagenReihenfolge($body["codes"] ?? null);
grundriss(true);
jsonAntwort(["stand" => grundrissStand()]);
case "etage-loeschen":
etageLoeschen($body["code"] ?? "");
grundriss(true);
jsonAntwort(["stand" => grundrissStand()]);
case "raum-speichern":
$nr = raumSpeichern($body);
grundriss(true);
jsonAntwort(["nr" => $nr, "stand" => grundrissStand()]);
case "raum-position":
$pos = raumPosition($body["nr"] ?? 0, $body["x"] ?? null, $body["y"] ?? null);
grundriss(true);
jsonAntwort(["position" => $pos, "stand" => grundrissStand()]);
case "raum-loeschen":
raumLoeschen($body["nr"] ?? 0);
grundriss(true);
jsonAntwort(["stand" => grundrissStand()]);
case "energiefluss-save":
// Zurück kommt der gespeicherte Stand - ohne die Einträge,
// die dem Katalog entsprechen.
energieflussSpeichern($body["knoten"] ?? null);
jsonAntwort(["einstellungen" => (object)energieflussLaden()]);
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);
}
}
if ($action === "kacheln") {
try {
jsonAntwort([
"kacheln" => kachelListe(),
// Alle Symbolnamen - die üblichen zuerst und mit Beschriftung.
"symbole" => kachelIconNamen(),
// Auswahlliste je Geraet - dieselbe Quelle wie im Automatik-Editor.
"katalog" => kachelKatalog(),
// Fuer die Eingabe von Hand, wenn etwas nicht im Katalog steht.
"topics" => kachelTopicVorschlaege(),
"tabelleDa" => grundrissDa(),
"max" => KACHEL_MAX,
]);
} catch (Throwable $e) {
jsonAntwort(["error" => "Kacheln nicht abrufbar: " . $e->getMessage()], 500);
}
}
if ($action === "grundriss") {
try {
jsonAntwort(grundrissStand());
} catch (Throwable $e) {
jsonAntwort(["error" => "Grundriss nicht abrufbar: " . $e->getMessage()], 500);
}
}
if ($action === "energiefluss") {
try {
jsonAntwort(["einstellungen" => (object)energieflussLaden()]);
} catch (Throwable $e) {
jsonAntwort(["error" => "Übersicht nicht abrufbar: " . $e->getMessage()], 500);
}
}
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()]);
} catch (Throwable $e) {
jsonAntwort(["error" => "Geräte nicht abrufbar: " . $e->getMessage()], 500);
}
}
if ($action === "list") {
try {
jsonAntwort(["zeilen" => preiseLaden($_GET["reihe"] ?? "")]);
} catch (InvalidArgumentException $e) {
jsonAntwort(["error" => $e->getMessage()], 400);
} catch (Throwable $e) {
jsonAntwort(["error" => "Laden fehlgeschlagen: " . $e->getMessage()], 500);
}
}
jsonAntwort(["error" => "Unbekannte Aktion."], 400);