Liegt in ~/Backup-scripts/solarlog-rollup.sh. Dazu die Eigenschaften der Rohdaten, die man beim Rechnen kennen muss (Momentanleistungen, gemischte Einheiten und Vorzeichen). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
211 lines
11 KiB
Markdown
211 lines
11 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`, `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.
|
|
|
|
### 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 |
|