Neuer Reiter mit Plan-Editor: Etagen anlegen, umbenennen, sortieren, loeschen und als Standard markieren; Grundrissbild hochladen; Raeume mit Name, Kuerzel und Thermostat-Zweig anlegen und bearbeiten; Kacheln im Plan ziehen oder einen Raum ohne Kachel mit "Platzieren" und einem Klick setzen. Loest tools/kachelpositionen.php ab - Raster, Geisterkachel und die Umrechnung Mitte -> linke obere Ecke sind von dort uebernommen. - restricted/grundriss.php: alle Pruefungen und Schreibzugriffe. Eine Etage mit Raeumen oder Automatiken laesst sich nicht loeschen, die Ablehnung nennt die Namen. Raeume loeschen laesst ihre Geraete ohne Raum. - Grundrissbilder nach tiles/grundriss/ (beschreibbar, nicht im Git), nur PNG/JPEG/WebP, am Inhalt geprueft; SVG bewusst nicht. Prüfsumme im Dateinamen, alte hochgeladene Bilder werden weggeraeumt. - ajax/settings.php: ?action=grundriss und sieben POST-Aktionen, jede antwortet mit dem ganzen neuen Stand. - "Werte" springt in den Reiter Home-Kacheln und oeffnet die Kachel; Home-Kacheln und Geraete laden nach einer Grundriss-Aenderung nach. - Startseite ohne Etage mit platziertem Raum: Hinweis auf die Einstellungen statt einer leeren Zeichenflaeche. - README: Grundriss als Inhalt statt Code, neue Dateien und Endpunkte, Reihenfolge der Schema-Dateien fuer ein neues Haus. Backend mit einer Probe-Etage durchgespielt (auch Fehlerfaelle, Upload, Aufraeumen); Oberflaeche auf einer Probeseite mit echten Daten und Zeiger-Ereignissen geprueft: Ziehen, Antippen, Platzieren, neuer Raum. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
294 lines
14 KiB
PHP
294 lines
14 KiB
PHP
<?php
|
||
/**
|
||
* Einstellungsseite: Parameter, die von Hand gepflegt werden.
|
||
*
|
||
* Gegliedert in Reiter, einer je Thema - Preise und die Anzeigewerte der
|
||
* Home-Kacheln. Die Liste $reiter unten ist die einzige Stelle, die davon
|
||
* weiss: ein neues Thema ist ein Eintrag dort plus der Inhalt, alles Uebrige
|
||
* (Navigation, Umschalten, Merken des Reiters in der Adresse) ergibt sich
|
||
* daraus. Vorher stand jedes Thema als Karte untereinander; bei drei Themen
|
||
* waere die Seite ein Scrollband geworden.
|
||
*
|
||
* Innerhalb eines Reiters bleibt es bei Karten - eine je Tabelle bzw. je
|
||
* Etage.
|
||
*
|
||
* 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");
|
||
require_once(__DIR__ . "/kacheln.php");
|
||
require_once(__DIR__ . "/reiter.php");
|
||
// Nur wegen SKODA_APP_LINK - der Weg zu den Schlüsseln gehört neben das Feld,
|
||
// in das sie eingetragen werden, und der Link soll nur an einer Stelle stehen.
|
||
require_once(__DIR__ . "/skodaKeys.php");
|
||
|
||
/*
|
||
* Die Reiter. Reihenfolge ist Anzeigereihenfolge; der erste ist der offene,
|
||
* solange die Adresse nichts anderes sagt (#kacheln).
|
||
*/
|
||
$reiter = [
|
||
["id" => "preise", "titel" => "Preise", "symbol" => "bi-currency-euro"],
|
||
["id" => "grundriss", "titel" => "Grundriss", "symbol" => "bi-house"],
|
||
["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"],
|
||
];
|
||
// "merken": der offene Reiter steht in der Adresse und uebersteht ein
|
||
// Neuladen - siehe reiterMerken() in js/solar/common.js.
|
||
$reiterOpt = ["name" => "einstellung", "merken" => true, "klasse" => "mb-3"];
|
||
$reiterOpt["aktiv"] = reiterAktiv($reiter, $reiterOpt);
|
||
|
||
$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; ?>
|
||
|
||
<!-- Reiter. Welcher offen ist, merkt sich die Adresse (#kacheln) -
|
||
ein Neuladen nach dem Speichern landet damit wieder dort. -->
|
||
<?= reiterLeiste($reiter, $reiterOpt) ?>
|
||
|
||
<div class="tab-content">
|
||
<?= reiterFlaecheAuf("preise", $reiterOpt) ?>
|
||
<?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; ?>
|
||
<?= reiterFlaecheZu() ?><!-- /Preise -->
|
||
|
||
<!-- ================================================== Grundriss ==== -->
|
||
<!-- Etagen, Räume und wo ihre Kacheln sitzen. Stand bis September 2026
|
||
als Tabelle in restricted/rooms.php; jetzt in homeMesh.floors und
|
||
homeMesh.rooms, gepflegt hier. Den Inhalt zeichnet
|
||
js/solar/grundriss.js aus ajax/settings.php?action=grundriss. -->
|
||
<?= reiterFlaecheAuf("grundriss", $reiterOpt) ?>
|
||
<div class="row">
|
||
<div class="col-12 col-xxl-11">
|
||
<div class="card mb-4" id="grundrissKarte">
|
||
<div class="card-header pb-2 pt-2">
|
||
<h3 class="card-title"><i class="bi bi-house me-2"></i>Etagen und Räume</h3>
|
||
<div class="card-tools">
|
||
<button type="button" class="btn btn-tool" id="grundrissNeuLaden" title="Neu laden">
|
||
<i class="bi bi-arrow-counterclockwise"></i>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div class="card-body">
|
||
<p class="text-secondary small mb-3">
|
||
Jede Etage hat ein Grundrissbild, darauf sitzen die Kacheln ihrer Räume.
|
||
Ein Raum ohne Kachel ist trotzdem ein Raum: Geräte lassen sich ihm
|
||
zuordnen, er erscheint nur nicht im Plan. Was auf einer Kachel steht,
|
||
wird unter „Home-Kacheln“ eingestellt.
|
||
</p>
|
||
<div id="grundrissFlaeche">
|
||
<p class="text-secondary small mb-0">Wird geladen …</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<?= reiterFlaecheZu() ?><!-- /Grundriss -->
|
||
|
||
<!-- ================================================ 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,
|
||
das ajax/settings.php?action=kacheln liefert - dieselbe Arbeits-
|
||
teilung wie bei den Preisen. -->
|
||
<?= reiterFlaecheAuf("kacheln", $reiterOpt) ?>
|
||
<div class="row">
|
||
<div class="col-12 col-xxl-9">
|
||
<div class="card mb-4" id="kachelKarte">
|
||
<div class="card-header pb-2 pt-2">
|
||
<h3 class="card-title"><i class="bi bi-grid-1x2 me-2"></i>Werte auf den Kacheln</h3>
|
||
<div class="card-tools">
|
||
<button type="button" class="btn btn-tool" id="kachelNeuLaden"
|
||
title="Neu laden">
|
||
<i class="bi bi-arrow-counterclockwise"></i>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div class="card-body">
|
||
<p class="text-secondary small mb-3">
|
||
Jede Kachel im Grundriss zeigt bis zu drei Messwerte. Ohne eigene
|
||
Angabe sind das bei Räumen mit Thermostat Soll, Ist und Feuchte,
|
||
sonst nur der Name. Was hier gesetzt wird, gilt stattdessen;
|
||
„Standard“ nimmt es wieder zurück. Wo Kacheln sitzen und welcher
|
||
Raum ein Thermostat hat, steht unter „Grundriss“.
|
||
</p>
|
||
<div id="kachelListe">
|
||
<p class="text-secondary small mb-0">Wird geladen …</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<?= reiterFlaecheZu() ?><!-- /Home-Kacheln -->
|
||
|
||
<!-- ===================================================== Geräte ==== -->
|
||
<!-- Die Zuordnung Gerät -> Raum lag vorher als Modal auf der Home-Seite.
|
||
Sie gehört hierher: sie wird selten geändert, sie ist eine
|
||
Einstellung, und hier ist Platz für das, was daneben gehört - wie
|
||
lange ein Gerät nichts mehr gemeldet hat, in welchen Automatiken es
|
||
steckt, und der Weg, eine Karteileiche loszuwerden. -->
|
||
<?= reiterFlaecheAuf("geraete", $reiterOpt) ?>
|
||
<div class="row">
|
||
<div class="col-12 col-xxl-10">
|
||
<div class="card mb-4" id="geraeteKarte">
|
||
<div class="card-header pb-2 pt-2">
|
||
<h3 class="card-title"><i class="bi bi-house-gear me-2"></i>Geräte und Räume</h3>
|
||
<div class="card-tools">
|
||
<button type="button" class="btn btn-tool" id="geraeteNeuLaden" title="Neu laden">
|
||
<i class="bi bi-arrow-counterclockwise"></i>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div class="card-body">
|
||
<p class="text-secondary small mb-3">
|
||
Der Raum ordnet die Geräteliste im Automatik-Editor und bestimmt,
|
||
welche Messwerte bei einer Home-Kachel oben stehen. Er wird von Hand
|
||
vergeben und bleibt bei einem neuen Suchlauf erhalten – der Suchlauf
|
||
legt nur an und aktualisiert, er löscht nichts. Abgebaute Geräte
|
||
bleiben deshalb stehen, bis sie hier entfernt werden.
|
||
</p>
|
||
<div id="geraeteListe">
|
||
<p class="text-secondary small mb-0">Wird geladen …</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<?= reiterFlaecheZu() ?><!-- /Geräte -->
|
||
|
||
<!-- =================================================== Fahrzeug ==== -->
|
||
<!-- Die Zugangsschlüssel der MyŠkoda-Anbindung. Sie laufen ab und
|
||
müssen dann getauscht werden - vorher hieß das: per SSH auf die NAS
|
||
und skoda.conf im Editor öffnen.
|
||
|
||
Die Zeilen zeichnet js/solar/settings.js aus dem, was
|
||
ajax/settings.php?action=skoda-keys liefert. Was dort NICHT
|
||
mitkommt, sind die Schlüssel selbst: die Seite steht im Heimnetz
|
||
ohne Anmeldung offen, und ein solcher Schlüssel steuert das
|
||
Fahrzeug von überall auf der Welt. Zu sehen ist nur, ob einer
|
||
hinterlegt ist und auf welche vier Zeichen er endet. -->
|
||
<?= reiterFlaecheAuf("fahrzeug", $reiterOpt) ?>
|
||
<div class="row">
|
||
<div class="col-12 col-xxl-8">
|
||
<div class="card mb-4" id="skodaKarte">
|
||
<div class="card-header pb-2 pt-2">
|
||
<h3 class="card-title"><i class="bi bi-key me-2"></i>Zugang zum Fahrzeug</h3>
|
||
<div class="card-tools">
|
||
<button type="button" class="btn btn-tool" id="skodaNeuLaden" title="Neu laden">
|
||
<i class="bi bi-arrow-counterclockwise"></i>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div class="card-body">
|
||
<p class="text-secondary small mb-3">
|
||
Die Schlüssel werden in der MyŠkoda-App erzeugt, gelten nur für die
|
||
dort ausgewählten Fahrzeuge und laufen nach einiger Zeit ab. Ein neuer
|
||
wird hier eingetragen und sofort einmal ausprobiert – was die API
|
||
ablehnt, landet gar nicht erst in der Konfiguration.
|
||
</p>
|
||
<p class="mb-3">
|
||
<a class="btn btn-sm btn-outline-primary"
|
||
href="<?= SKODA_APP_LINK ?>" target="_blank" rel="noopener">
|
||
<i class="bi bi-box-arrow-up-right me-1"></i>Schlüssel in der MyŠkoda-App verwalten
|
||
</a>
|
||
</p>
|
||
<div id="skodaListe">
|
||
<p class="text-secondary small mb-0">Wird geladen …</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<?= reiterFlaecheZu() ?><!-- /Fahrzeug -->
|
||
</div><!-- /tab-content -->
|
||
|
||
</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>
|