doku/meteogramm.md: der Weg der Daten, welche Groessen links vom Jetzt-Strich gemessen und welche vorhergesagt sind, die vier Tabellen samt der Regel, dass nichts geloescht wird, das Tupel-Schema des Endpunkts, das Katalog-Vokabular mit dem Dreizeiler "so kommt eine Spur dazu", und die beiden Entscheidungen beim Zeichnen (ein SVG ueber alle Felder, Wolkenband als Farbverlauf statt als Raster). Dazu zwei Fallen, die sonst niemand wiederfindet: die Archiv-Schnittstelle von Open-Meteo rechnet alle Zeiten mit dem heute gueltigen Zeitzonenversatz um, und die Spaltenlage des Endpunkts steht an zwei Stellen und muss zueinander passen. Richtiggestellt: doku/datenbank.md und README.md nannten Open-Meteo als Quelle von weatherHours/weatherDays. Das war nie so - es war OpenWeatherMap, und seit dem 27.10.2024 gar nichts mehr. Beide Zeilen warfen ausserdem Messung und Vorhersage in einen Topf; sie sind jetzt getrennt, mit dem richtigen Schreiber je Tabelle. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
216 lines
12 KiB
Markdown
216 lines
12 KiB
Markdown
# 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` | ~195.000 | die eigene Wetterstation, alle 5 min | die Station selbst, über `/volume1/web/weatherStation.php` |
|
||
| `weatherHours`, `weatherDays` | ~40.000 / 1.600 | Wettervorhersage **und Archiv**, stündlich bzw. täglich (→ [meteogramm.md](meteogramm.md)) | `gatherForecastData.py` (Open-Meteo) |
|
||
| `weatherTilted`, `weatherForecastLog` | je nach Nutzung | Einstrahlung in Modulebene; die erste Vorhersage je Stunde | `gatherForecastData.py` |
|
||
| `daylight` | 663 | Sonnenauf- und -untergang, nur bis morgen | `solarManager.py` (gerechnet, nicht abgerufen) |
|
||
| `simPower` | ~82.000 | Ertragsprognose, 7 Tage voraus | Solcast, 4×/Tag über `/volume1/web/gatherSolar.php` |
|
||
| `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<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.
|
||
|
||
### 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. **`weatherHours` und `weatherDays` stehen bewusst nicht
|
||
dabei** — sie sind schon stündlich, und ihre Strahlungswerte sind die eine
|
||
Hälfte des Datenpaares für eine künftige Ertragsprognose (die andere ist
|
||
`EnergyFlow_hourly.pv_kwh`). 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 |
|