Files
adminandClaude Opus 5 df48432133 Benachrichtigungen: Geraet, Abos und der Reiter Meldungen
Der Geraetesuchlauf legt "Benachrichtigungen" an (benachrichtigung_module.py),
ein Kommando je Kanal. Damit kann jede Automatik melden, ohne dass Editor,
Zeitleiste oder Datenbank etwas davon wissen muessten.

Fuer die Meldung aufs Handy braucht es keine App: Der Browser meldet sich
unter Einstellungen -> Meldungen selbst an (sw.js, ajax/push.php), das Abo
steht in homeMesh.push_abos, verschickt wird vom Runner. Auf dem iPhone geht
es erst ab der Ablage auf dem Home-Bildschirm - der Reiter sagt das auch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-21 08:37:31 +02:00

371 lines
18 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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" => "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"],
["id" => "meldungen", "titel" => "Meldungen", "symbol" => "bi-bell"],
];
// "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 -->
<!-- ================================================== Ü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,
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 -->
<!-- Meldungen. Web Push braucht kein fremdes Konto und keine App: Der
Browser meldet sich selbst an, die NAS schickt verschlüsselt an den
Push-Dienst seines Herstellers. Jedes Gerät meldet sich einzeln an -
ein Abo gehört dem Browser, nicht dem Benutzer. -->
<?= reiterFlaecheAuf("meldungen", $reiterOpt) ?>
<div class="row">
<div class="col-12 col-xxl-8">
<div class="card mb-4" id="pushKarte">
<div class="card-header pb-2 pt-2">
<h3 class="card-title"><i class="bi bi-bell me-2"></i>Benachrichtigungen</h3>
<div class="card-tools">
<button type="button" class="btn btn-tool" id="pushNeuLaden" title="Neu laden">
<i class="bi bi-arrow-counterclockwise"></i>
</button>
</div>
</div>
<div class="card-body">
<p class="text-secondary small mb-3">
Automatiken können Meldungen schicken als Nachricht auf dieses Gerät
oder als E-Mail. Für die Meldung auf dem Gerät braucht es keine App:
Der Browser meldet sich hier an und zeigt die Meldung auch, wenn die
Seite zu ist. Auf dem iPhone muss die Seite dafür einmal über
„Teilen ▸ Zum Home-Bildschirm“ abgelegt und von dort geöffnet werden.
</p>
<div id="pushListe">
<p class="text-secondary small mb-0">Wird geladen …</p>
</div>
</div>
</div>
</div>
</div>
<?= reiterFlaecheZu() ?><!-- /Meldungen -->
</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>