# 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 (→ [byd.md](byd.md)) | `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
alle 5 min, Momentanleistung"] -->|"nachts, Rollup"| B["EnergyFlow_hourly
Stunden in kWh"] A --> C["stats_daily
Tageswerte + vorberechnete Größen"] A -. "nach 12 Monaten ausgedünnt" .-> A B --> H2["Historie: Jahr, Jahrzehnt"] A --> H1["Historie: Monat
der laufende Tag fehlt im Archiv"] C --> S["Jahresstatistik
ajax/getStats.php"] ``` 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. ### Der Rollup-Job `~/Backup-scripts/solarlog-rollup.sh` (Schema dazu: `solarlog-rollup.sql`) hat drei Betriebsarten: | Aufruf | Was er tut | |---|---| | `fill [--from … --to …]` | Stunden- und Tageswerte fortschreiben. Idempotent (`INSERT … ON DUPLICATE KEY UPDATE`), darf jederzeit wiederholt werden | | `thin [--dry-run] [--months 12]` | Rohdaten älter als zwölf Monate ausdünnen — **nur**, wenn für den Monat ein Aggregat vorliegt. Vorher wird der betroffene Zeitraum monatsweise nach `/volume1/docker/solarLog_history` weggesichert | | `status` | zeigt, bis wohin aggregiert und ab wann ausgedünnt ist | Ausgedünnt werden `EnergyFlow`, `Heater` und `weatherStation`; das Zielraster beträgt 15 Minuten. Was man beim Rechnen auf den Rohdaten wissen muss, steht ausführlich im Kopf der SQL-Datei — die wichtigsten Punkte: * **Alles sind Momentanleistungen**, gemittelt über rund fünf Minuten. kWh entstehen nur durch Integration über die Zeit, nie durch Mittelwerte über Zeilen. * **Die Einheiten sind gemischt:** die Wallbox-Spalten liefern Kilowatt, alle übrigen Leistungsspalten Watt. * **Die Vorzeichen sind es auch:** `totalConsumption` negativ = Hausverbrauch, `battP` negativ = Laden, und `PL*_OG` kommt negativ, während `PL*_EG` und `PL*_UG` positiv sind. * Messlücken über 15 Minuten zählen nicht als durchgehende Leistung. 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 | `~/Backup-scripts/solarlog-rollup.sh` (läuft als root im MariaDB-Container) | 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 |