datenbank.md: die drei Schemata, wer was schreibt, die drei Stufen der Verdichtung und welche Tabelle fuer welche Auswertung taugt. einstellungen.md: was jeder Reiter speichert, welche Regeln das Modell durchsetzt (feste Etagenkuerzel, Loeschsperren, Schluessel gehen nie zurueck) und was die Seite bewusst nicht kann. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
225 lines
10 KiB
Markdown
225 lines
10 KiB
Markdown
# Die Einstellungsseite
|
||
|
||
Alles, was das Haus beschreibt statt es zu programmieren: Preise, Etagen und
|
||
Räume, die Solar-Übersicht, die Werte auf den Kacheln, die Zuordnung der
|
||
Geräte und der Zugang zum Fahrzeug. **Was hier eingetragen wird, steht in der
|
||
Datenbank — nicht im Quelltext.** Eine zweite Installation braucht deshalb
|
||
keine Codeänderung, sondern einen Nachmittag auf dieser Seite.
|
||
|
||
| Reiter | Speichert nach | Modell (PHP) | Maske (JS) |
|
||
|---|---|---|---|
|
||
| Preise | `solarLog.gridCosts`, `gasCosts`, `fuelCosts` | `restricted/costs.php` | `settings.js` |
|
||
| Grundriss | `homeMesh.floors`, `rooms` | `restricted/grundriss.php`, `rooms.php` | `grundriss.js` |
|
||
| Übersicht | `homeMesh.energiefluss` | `ajax/energiefluss.php` | `energieflussEinstellungen.js` |
|
||
| Home-Kacheln | `homeMesh.rooms.werte` (JSON) | `restricted/kacheln.php` | `settings.js` |
|
||
| Geräte | `homeMesh.actors.room_id` | `restricted/automations.php` | `settings.js` |
|
||
| Fahrzeug | `skoda.conf` (außerhalb des Web-Roots) | `restricted/skodaKeys.php` | `settings.js` |
|
||
|
||
Ein Endpunkt für alles: `ajax/settings.php?action=…`. Die Maske prüft nichts,
|
||
was das Modell nicht auch prüfte — sie zeigt nur die Meldung, die von dort
|
||
kommt.
|
||
|
||
```mermaid
|
||
flowchart LR
|
||
UI["settings.php<br/><small>Reiter aus reiter.php</small>"] --> JS["settings.js · grundriss.js<br/>energieflussEinstellungen.js"]
|
||
JS -->|"GET ?action=…"| EP["ajax/settings.php"]
|
||
JS -->|"POST ?action=…"| EP
|
||
EP --> M1["costs.php"] --> SL[("solarLog")]
|
||
EP --> M2["grundriss.php / rooms.php"] --> HM[("homeMesh")]
|
||
EP --> M3["kacheln.php"] --> HM
|
||
EP --> M4["automations.php<br/><small>geraeteListe, geraetLoeschen</small>"] --> HM
|
||
EP --> M5["skodaKeys.php"] --> CONF[["skoda.conf<br/><small>/volume1/homes/wagner/SolarManager</small>"]]
|
||
```
|
||
|
||
---
|
||
|
||
## 1. Preise
|
||
|
||
Drei Tabellen desselben Zuschnitts: **je Zeile ein Stichtag, kein Enddatum.**
|
||
Das Ende holt sich die Auswertung beim nächsten Eintrag (`LEAD()`), damit
|
||
eine Preiserhöhung eine Zeile ist und nicht zwei Datumsfelder, die
|
||
auseinanderlaufen können.
|
||
|
||
| Reihe | Felder | Wofür |
|
||
|---|---|---|
|
||
| Strom | Arbeitspreis, Einspeisung (ct/kWh), Grundpreis (€/Jahr) | Jahresstatistik |
|
||
| Gas | Gaspreis (ct/l), Energieinhalt (kWh/l) | Ersparnis des Heizstabs |
|
||
| Benzin | Preis (€/l), Verbrauch Verbrenner (l/100 km), Verbrauch Elektro (kWh/100 km) | „Benzin gespart“ |
|
||
|
||
Zwei Entscheidungen, die man beim Erweitern kennen sollte:
|
||
|
||
* **Gespeichert wird in Euro, eingegeben in Cent** (`faktor` je Feld). Auf
|
||
der Rechnung stehen 31,63 ct/kWh — niemand tippt gern 0,3163.
|
||
* **Nicht jedes Feld ist ein Preis.** Energieinhalt und Verbräuche stehen
|
||
hier, weil sie am Brennstoff und am Fahrzeug hängen, nicht an der Physik.
|
||
Beim Auto bewusst als zwei bekannte Verbräuche statt als „kWh je Liter“ —
|
||
diese Zahl steht auf keinem Datenblatt.
|
||
|
||
Eine weitere Tabelle desselben Zuschnitts ist ein Eintrag in
|
||
`preisReihen()`; Endpunkt, Maske und Speicherlogik bleiben unverändert.
|
||
|
||
---
|
||
|
||
## 2. Grundriss
|
||
|
||
Der Reiter pflegt `floors` und `rooms` — und damit gleichzeitig das Menü, die
|
||
Startseite, die Kacheln, die MQTT-Abos und die Etagen-Auswahl im
|
||
Automatik-Editor.
|
||
|
||
```mermaid
|
||
flowchart TB
|
||
E["Etage<br/><small>code, Bezeichnung, Reihenfolge, Bild, Standard</small>"] --> R["Raum<br/><small>Name, Kürzel, Thermostat-Zweig</small>"]
|
||
R --> K["Kachel<br/><small>x/y auf der Zeichenfläche 400 × 300</small>"]
|
||
R --> G["Geräte<br/><small>actors.room_id</small>"]
|
||
E --> A["Automatiken<br/><small>automations.floor</small>"]
|
||
```
|
||
|
||
Regeln, die das Modell durchsetzt:
|
||
|
||
* **Das Kürzel einer Etage ist nach dem Anlegen fest.** Es steht in URLs,
|
||
SVG-Ids und `automations.floor`; bis zu vier Großbuchstaben oder Ziffern.
|
||
* **Gelöscht wird nur, was leer ist.** Eine Etage mit Räumen oder Automatiken
|
||
wird abgelehnt — mit der Liste dessen, was im Weg steht. Stilles
|
||
Mitlöschen wäre Datenverlust.
|
||
* **Die Raumnummer überlebt alles.** Umbenennen und Etagenwechsel ändern sie
|
||
nicht, deshalb behalten Geräte, Regler und Automatiken ihren Raum.
|
||
* **Kein `x`/`y` heißt: keine Kachel.** Die Position wird im Plan gezogen,
|
||
Maße wie in `home.php` (Fläche 400 × 300, Kachel 24 × 19).
|
||
* **Thermostat-Zweig** ist der MQTT-Pfad des Raums. Vorschläge kommen aus den
|
||
Topics, die gerade auf `/Temp[degC]` enden.
|
||
* **Grundrissbilder**: PNG, JPEG oder WebP, geprüft am Inhalt
|
||
(`getimagesize`), nicht an der Endung. Hochgeladene landen unter
|
||
`tiles/grundriss` — dem einen Verzeichnis, in das der Webserver schreiben
|
||
darf, und bewusst außerhalb von Git: Ein Grundriss ist Inhalt dieses
|
||
Hauses, nicht Teil der Anwendung. Der Dateiname trägt einen Teil der
|
||
Prüfsumme, damit ein neues Bild eine neue Adresse bekommt und nicht im
|
||
Browser-Zwischenspeicher hängen bleibt.
|
||
|
||
---
|
||
|
||
## 3. Übersicht (Energiefluss)
|
||
|
||
Die Solar-Übersicht hat einen **Katalog im Code**
|
||
(`js/solar/energieflussKatalog.js`) — jeder Kreis genau einmal, mit Name,
|
||
Symbol, Farbe, Elternkreis, Lage und Ringskala. In der Datenbank steht nur,
|
||
**was davon abweicht**.
|
||
|
||
```mermaid
|
||
flowchart LR
|
||
KAT["energieflussKatalog.js<br/><small>der Normalfall</small>"] --> MIX["setzeEinstellungen()"]
|
||
DB[("homeMesh.energiefluss<br/><small>nur Abweichungen</small>")] --> MIX
|
||
MIX --> BILD["gezeichnete Übersicht"]
|
||
```
|
||
|
||
Änderbar sind: Kreis an/aus, Name, Lage (im Vorschaubild ziehen) und
|
||
Optionen, etwa welche Einträge von `P_PVn` zu welcher PV-Fläche gehören.
|
||
„Alles auf Katalog zurücksetzen“ löscht schlicht alle Zeilen.
|
||
|
||
Der Vorteil dieser Trennung: Eine neue Installation zeigt sofort etwas
|
||
Sinnvolles, ohne dass jemand eine Tabelle füllt — und ein Update des Katalogs
|
||
erreicht alle Installationen, ohne die Anpassungen zu überschreiben.
|
||
|
||
> Die Hochkant-Lage (`hochX`/`hochY`) steht nur im Katalog. Wer Kreise
|
||
> verschiebt, ändert also die Queransicht;
|
||
> siehe [README](../README.md) → Energiefluss.
|
||
|
||
---
|
||
|
||
## 4. Home-Kacheln
|
||
|
||
Welche Werte auf einer Kachel stehen, steht als JSON in `rooms.werte` —
|
||
höchstens drei je Kachel. Ohne Eintrag gilt `kachelVorgabe()`: mit
|
||
Thermostat Soll, Ist und Feuchte, sonst nichts.
|
||
|
||
Eine Wertdefinition hat überall dieselbe Form, damit zwischen Maske,
|
||
Datenbank und Anzeige nichts übersetzt werden muss:
|
||
|
||
| Feld | Bedeutung |
|
||
|---|---|
|
||
| `topic` | MQTT-Topic, aus dem der Wert kommt |
|
||
| `pfad` | Schlüssel in einer JSON-Nachricht (die Shelly-Zähler schicken mehrere Werte in einer) |
|
||
| `wechselrichter`, `feld` | statt `topic`: Summe benannter Wechselrichter unter `solarManager/invertersN` |
|
||
| `format` | `leistung` (Quelle meldet Watt) oder `leistung_kw` (Quelle meldet Kilowatt) — skaliert selbst zwischen W und kW |
|
||
| `einheit`, `stellen` | sonst: Einheit dahinter, Nachkommastellen |
|
||
| `negativ` | Vorzeichen drehen, für verkehrt herum eingebaute Zähler |
|
||
| `gross` | die Zahl steht groß auf der Kachel |
|
||
| `icon` | Name eines Bootstrap-Icons ohne `bi-` |
|
||
|
||
Gelesen wird das im Browser von `homeSVG.kachelWert()`. Warum die
|
||
Wechselrichter über ihren **Namen** und nicht über die Nummer gesucht
|
||
werden: `inverters0` ist eine Position in einer Liste und sagt nichts über
|
||
das Dach — ändert sich die Reihenfolge, stünde sonst stillschweigend das
|
||
falsche Dach auf der Kachel.
|
||
|
||
---
|
||
|
||
## 5. Geräte
|
||
|
||
Die Liste zeigt je Gerät: Beschriftung, Art, Raum, einen Raumvorschlag aus
|
||
dem MQTT-Topic, die Zahl der Messwerte und Kommandos, wann zuletzt etwas
|
||
gemeldet wurde — **und in welchen Automatiken es steckt**.
|
||
|
||
Genau deshalb liegt die Liste im Modell und nicht in der Maske: Wer über das
|
||
Löschen entscheidet, muss die Verweise kennen.
|
||
|
||
| Handlung | Regel |
|
||
|---|---|
|
||
| Raum zuordnen | `actors.room_id`; ordnet die Geräteliste im Automatik-Editor und bestimmt, welche Messwerte bei einer Kachel oben stehen |
|
||
| Gerät löschen | nur, wenn **keine** Automatik darauf zeigt. Sonst Ablehnung mit den Namen der Automatiken. Gelöscht wird von innen nach außen: Parameter, Kommandos, Messwerte, Gerät — in einer Transaktion |
|
||
| Wieder da? | Steht das Gerät noch im Netz, findet es der nächste Suchlauf wieder. Das ist kein Fehler, sondern sein Zweck |
|
||
|
||
Der Suchlauf selbst löscht nie (→ [datenbank.md](datenbank.md#geräte)) —
|
||
deshalb braucht es diesen Papierkorb überhaupt.
|
||
|
||
---
|
||
|
||
## 6. Fahrzeug
|
||
|
||
Hier werden die Schlüssel der MyŠkoda-API hinterlegt. Sie stehen in
|
||
`skoda.conf` **außerhalb des Web-Roots**, weil sie auch `gatherSkodaData.py`
|
||
im SolarManager liest.
|
||
|
||
**Ein Schlüssel geht nie an den Browser zurück.** Die Seite steht im
|
||
Heimnetz ohne Anmeldung offen, und so ein Schlüssel steuert das Fahrzeug von
|
||
überall auf der Welt — Laden, Klimatisierung, Standort. Die Maske sieht nur,
|
||
*ob* einer hinterlegt ist und auf welche vier Zeichen er endet. Das genügt,
|
||
um zwei auseinanderzuhalten, und taugt zu nichts sonst. Geschrieben wird nur
|
||
in eine Richtung.
|
||
|
||
Dazu gehört die Aufteilung des Kontingents (20 Anfragen je Stunde für das
|
||
ganze Konto, nicht je Schlüssel):
|
||
|
||
| Zweck | Anteil |
|
||
|---|---|
|
||
| Abruf des Fahrzeugzustands (`gatherSkodaData.py`) | 14 / Stunde |
|
||
| Befehle aus der Oberfläche (`ajax/skodaCmd.php`) | 4 / Stunde |
|
||
| Reserve | 2 / Stunde |
|
||
|
||
Ein eigener Befehlsschlüssel trennt deshalb nicht die Kontingente, sondern
|
||
nur, wer womit fragt — er lässt sich einzeln widerrufen.
|
||
|
||
---
|
||
|
||
## 7. Was die Seite bewusst **nicht** kann
|
||
|
||
* **Geräte anlegen.** Das macht der Suchlauf
|
||
(`restricted/deviceDiscovery/`); von Hand angelegte Geräte hätten keine
|
||
Verbindung zur Wirklichkeit.
|
||
* **Automatiken bearbeiten.** Dafür gibt es den Editor auf der Home-Seite
|
||
(→ [automatiken.md](automatiken.md#2-der-editor)).
|
||
* **Die Standheizung des Fahrzeugs starten.** Das verlangt die
|
||
Sicherheits-PIN; sie dauerhaft auf der NAS zu hinterlegen ist eine andere
|
||
Größenordnung als ein API-Schlüssel und soll bewusst entschieden werden.
|
||
|
||
---
|
||
|
||
## 8. Wo fange ich an, wenn ich …
|
||
|
||
| Vorhaben | Ort |
|
||
|---|---|
|
||
| … eine weitere Preistabelle | Eintrag in `preisReihen()` (`restricted/costs.php`) |
|
||
| … ein neues Feld auf einer Kachel | `kachelWertPruefen()`/`kachelKatalog()` in `kacheln.php` und `homeSVG.kachelWert()` in `homeMQTT.js` |
|
||
| … eine Etage, einen Raum, ein Grundrissbild | nur diese Seite — kein Code |
|
||
| … einen weiteren Reiter | `restricted/settings.php` (Fläche über `reiter.php`), Endpunkt in `ajax/settings.php`, Modell in `restricted/`, Maske in `settings.js` |
|
||
| … einen weiteren Zugangsschlüssel verwalten | `restricted/skodaKeys.php` als Vorbild: Modell kennt die Datei, Maske sieht nur den Stand |
|