Doku: Datenbanken und Einstellungsseite ergaenzt

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>
This commit is contained in:
2026-09-21 07:57:35 +02:00
co-authored by Claude Opus 5
parent a0a8ad04de
commit 93ec545aaa
4 changed files with 417 additions and 5 deletions
+185
View File
@@ -0,0 +1,185 @@
# Die Datenbanken
Drei Schemata auf derselben MariaDB der NAS (Port 3310). Die Trennung ist
keine Ordnungsliebe, sondern Zuständigkeit:
| Schema | Inhalt | Kennzeichen |
|---|---|---|
| **`homeMesh`** | Geräte, Automatiken, Grundriss — **was gilt** | klein, viele Fremdschlüssel, wird von Hand gepflegt |
| **`solarLog`** | Messreihen, Statistik, Preise — **was war** | groß, schreibt fast nur die NAS, wird verdichtet |
| **`Logins`** | Passkeys und Einmal-Links | winzig, sicherheitsrelevant |
Wer von wo verbindet:
| Aufrufer | Funktion / Datei | Zugang |
|---|---|---|
| Weboberfläche | `meshDb()` (`restricted/meshdb.php`) | `homeMesh` |
| Weboberfläche | `solarDb()` (`restricted/costs.php`) | `solarLog` |
| Weboberfläche | `commandDb()` (`restricted/commands.php`) | `homeMesh`, für Kommandos |
| Weboberfläche | `checkLogin()` (`helper.php`) | `Logins` |
| NAS-Prozesse | `config.ini` im SolarManager | beide, eigene Benutzer |
Die Zugangsdaten stehen in `restricted/mysql.php` (Web) bzw. `config.ini`
(NAS) — beides nicht im Git.
---
## 1. `homeMesh` — was gilt
```mermaid
erDiagram
floors ||--o{ rooms : "hat"
rooms ||--o{ actors : "room_id"
actors ||--o{ actor_states : "meldet"
actors ||--o{ actor_commands : "kann"
actor_commands ||--o{ command_parameters : "nimmt"
state_types ||--o{ actor_states : "Datentyp"
automations ||--o{ automation_conditions : "wenn"
automations ||--o{ automation_actions : "dann"
automations ||--o{ automation_log : "Protokoll"
automation_conditions }o--|| actor_states : "vergleicht"
automation_actions }o--|| actor_commands : "schickt"
automation_actions ||--o{ automation_action_params : "mit"
floors ||--o{ automations : "Reiter"
energiefluss }o--|| floors : "keine Beziehung, nur Abweichungen"
```
### Geräte
| Tabelle | Inhalt | Geschrieben von |
|---|---|---|
| `actors` | ein Gerät je Zeile. `url` entscheidet den Weg: `mqtt://`, `http://`, `wled://`, `io://`/`rts://`/`internal://`/`ogp://` (Tahoma), `Logic`, `Automatik`. `room_id` verknüpft mit `rooms` | Gerätesuchlauf; `room_id` von Hand (Einstellungen → Geräte) |
| `actor_states` | alles Lesbare. `url` ist **entweder** ein MQTT-Topic **oder** ein Feldname im HTTP-JSON, `value_path` der Schlüssel darin, `current_value` der letzte Wert | Suchlauf (Adressen), Runner (Werte) |
| `actor_commands`, `command_parameters` | alles Schaltbare und die Parameter dazu (`url` = Stelle im Befehl) | Suchlauf |
| `state_types` | Datentypen: `integer`, `float`, `bool`, `string`, `time`, `date`, `datetime`, `deltatime`, `elapsed` … | fest |
Zwei Dinge, die immer wieder überraschen:
* **`sensors`/`sensor_states` sind unbenutzt.** Der Suchlauf legt auch reine
Messgeräte in `actors`/`actor_states` ab — ein Gerätebegriff für alles.
* **Der Suchlauf löscht nie** (`clear_tables = false`). Er schreibt mit
`ON DUPLICATE KEY UPDATE` auf den URLs. Ein `TRUNCATE` würde neue IDs
vergeben und **alle Automatiken auf falsche Geräte zeigen lassen**.
Karteileichen räumt man in den Einstellungen weg
(→ [einstellungen.md](einstellungen.md#5-geräte)).
### Automatiken
Eigenes Dokument: [automatiken.md](automatiken.md#1-datenmodell). Kurz:
`automations` (Rahmen + Laufzustand), `automation_conditions` (Auslöser,
`group_no` = UND/ODER), `automation_actions` + `automation_action_params`
(was geschickt wird), `automation_log` (30 Tage), `calendar_days` (Ferien
und Feiertage, jährlich per `fetch_calendar.py`).
### Grundriss und Anzeige
| Tabelle | Inhalt | Besonderheit |
|---|---|---|
| `floors` | Etagen: `code` (fest — steht in URLs, SVG-Ids und `automations.floor`), Bezeichnung, Reihenfolge, Grundrissbild, Standard-Etage | Schema `homeMesh_grundriss.sql` |
| `rooms` | feste Nummer `id`, Etage, Name, `kuerzel` (SVG-Ids), Kachelposition `x`/`y` (leer = keine Kachel), `thermostat` (MQTT-Zweig), `werte` (JSON, leer = Vorgabe) | die Nummer überlebt Umbenennen und Umziehen |
| `energiefluss` | **nur Abweichungen** der Solar-Übersicht vom Katalog: `aktiv`, `name`, `x`/`y`, `optionen` | keine Zeile = Katalogwert; Schema `homeMesh_energiefluss.sql` |
Die drei `view_*`-Objekte sind Lesehilfen aus der Anfangszeit des
Gerätesuchlaufs; die Anwendung benutzt sie nicht.
---
## 2. `solarLog` — was war
Stand heute rund 170 MB, verteilt auf wenige große Reihen:
| Tabelle | Zeilen | Inhalt | Geschrieben von |
|---|---:|---|---|
| `EnergyFlow` | ~108.000 | Momentanleistungen alle 5 min: PV, Netz, Batterie, Etagen, Heizstab, Wallbox | `solarManager.py` |
| `EnergyFlow_hourly` | ~38.000 | Stundenarchiv in kWh — die Langzeitquelle | Rollup-Job |
| `stats_daily` | ~1.500 | Tageswerte für die Jahresstatistik, inklusive vorberechneter Größen | Rollup-Job |
| `Heater` | ~632.000 | Heizung und Heizstab | `solarManager.py` |
| `puffertemp` | ~247.000 | Puffertemperaturen | `solarManager.py` |
| `wasser`, `zisterne` | ~300.000 | Wasserzähler und Zisterne | `gatherWaterData.py` |
| `windrad`, `kiga_windrad` | ~255.000 | Windräder | `solarManager.py` |
| `weatherStation`, `weatherHours`, `weatherDays` | ~220.000 | eigene Wetterstation, Vorhersage | Wetterbrücke, Open-Meteo |
| `daylight` | 663 | Sonnenauf- und -untergang je Tag | Vorhersage-Job |
| `simPower` | ~82.000 | Ertragsprognose | Prognose-Job |
| `byd`, `byd_zellen` | 1.559 / 492 | BYD-Speicher: alle 5 min Ladestand, SOH, Temperaturen; alle 15 min 128 Zellspannungen und 64 Temperaturen | `gatherBYDData.py` |
| `skoda`, `skoda_raw`, `skoda_ladepunkte` | ~1.900 | Fahrzeugzustand, Rohantwort, Ladeverlauf im Minutentakt | `gatherSkodaData.py` |
| `gridCosts`, `gasCosts`, `fuelCosts` | 13 | Preiszeitreihen | Einstellungsseite |
| `car`, `Status`, `actors`, `sensors`, `autoActions*`, `WindradLog` | — | Altlasten aus der Vorgängerfassung | — |
### Drei Stufen der Verdichtung
```mermaid
flowchart LR
A["EnergyFlow<br/><small>alle 5 min, Momentanleistung</small>"] -->|"nachts, Rollup"| B["EnergyFlow_hourly<br/><small>Stunden in kWh</small>"]
A --> C["stats_daily<br/><small>Tageswerte + vorberechnete Größen</small>"]
A -. "nach 12 Monaten ausgedünnt" .-> A
B --> H2["Historie: Jahr, Jahrzehnt"]
A --> H1["Historie: Monat<br/><small>der laufende Tag fehlt im Archiv</small>"]
C --> S["Jahresstatistik<br/><small>ajax/getStats.php</small>"]
```
Wichtig beim Auswerten:
* **Für einen Monat auf `EnergyFlow` rechnen**, sonst fehlt der laufende Tag
(das Archiv wird nur nachts fortgeschrieben).
* **Für Jahre auf `EnergyFlow_hourly`**, weil die Rohdaten nach zwölf Monaten
ausgedünnt werden — wer dort rechnet, zeigt für alte Jahre zu wenig.
* **Für Kennzahlen auf `stats_daily`.** Manches lässt sich aus Tagessummen
gar nicht rekonstruieren, etwa der Eigenverbrauch oder der Solaranteil der
Wallbox: dafür muss je Messwert eine Bedingung ausgewertet werden.
* **Einspeisung kommt aus `gridPfeed`**, dem Zählerregister, nicht aus dem
Saldo `gridP`. Innerhalb eines Fensters gibt es Bezug und Einspeisung
gleichzeitig; der Saldo löscht kurze Bezüge weg.
* **`pv_fehlbetrag_kwh`** gleicht aus, was fehlt, wenn ein Wechselrichter die
Verbindung zur OpenDTU verliert: Senken sind dann vollständig gemessen, die
Erzeugung nicht — ohne die Korrektur wird der Direktverbrauch negativ.
Die Preistabellen tragen **einen Stichtag und kein Enddatum**; das Ende holt
sich die Auswertung mit `LEAD()` beim nächsten Eintrag. Weil ein Stichtag ein
Datum ist, kann ein Tag nie zwei Tarife haben — die Verdichtung auf Tage
verliert hier also nichts.
---
## 3. `Logins` — Zugang
| Tabelle | Inhalt |
|---|---|
| `users` | Passkeys, `authKey`, `lastAuth` |
| `addUser` | Einmal-Links, mit denen ein neues Gerät einen Passkey anlegen darf |
`checkLogin()` in `helper.php` entscheidet in dieser Reihenfolge:
1. **Heimnetz** (`isLocal()`) — freier Zugang, ohne Anmeldung.
2. Gültige Sitzung mit `authKey`, höchstens zwei Tage alt.
3. Sonst: Anmeldeseite.
Daraus folgt die wichtigste Sicherheitsregel dieses Projekts: **Was im
Heimnetz ohne Anmeldung sichtbar ist, darf keinen Schlüssel preisgeben.**
Deshalb gehen API-Schlüssel nie an den Browser zurück
(→ [einstellungen.md](einstellungen.md#6-fahrzeug)), und deshalb schaltet die
Oberfläche Geräte über den Server statt direkt.
---
## 4. Pflege und Sicherung
| Aufgabe | Wer | Takt |
|---|---|---|
| Rohdaten ausdünnen, Stundenarchiv und Tageswerte fortschreiben | Rollup-Job auf der NAS (`solarlog-rollup.sh`) | nachts |
| `automation_log` aufräumen | Runner selbst | täglich, 30 Tage Aufbewahrung |
| `calendar_days` füllen | `fetch_calendar.py` | jährlich per Cron |
| Datenbanksicherung | `backupDB.sh` | per Cron |
| Schema neu aufsetzen | `homeMesh_DB-layout.sql`, `homeMesh_automations.sql`, `homeMesh_grundriss.sql`, `homeMesh_energiefluss.sql` in dieser Reihenfolge, danach die Nachrüstskripte aus `SolarManager/autoActions/` | einmalig |
---
## 5. Wo fange ich an, wenn ich …
| Vorhaben | Ort |
|---|---|
| … eine neue Messreihe aufzeichnen | Tabelle in `solarLog` anlegen, Schreiber im SolarManager, Leser als `ajax/*.php` |
| … eine Kennzahl in die Jahresstatistik aufnehmen | Spalte in `stats_daily` (Rollup) **und** Eintrag in `ajax/getStats.php` |
| … wissen, warum eine Zahl in zwei Ansichten abweicht | zuerst prüfen, aus welcher Stufe sie stammt (roh, Stunde, Tag) |
| … ein Gerät endgültig loswerden | Einstellungen → Geräte; per SQL nur, wenn keine Automatik darauf zeigt |
| … eine zweite Installation aufsetzen | Schemata einspielen, dann Einstellungen → Grundriss und → Übersicht; Inhalte stehen in der Datenbank, nicht im Code |