Files
Smart-Dashboard/doku/datenbank.md
T
adminandClaude Opus 5 a4b25ba279 Doku: die Schnittstelle zur BYD-Batterie beschrieben
doku/byd.md: warum der Umweg über die BMU überhaupt nötig ist (der Gen24
kennt weder Zellen noch SOH noch die Zähler), der Ablauf der sechs Anfragen
in einer einzigen Verbindung, und vor allem die Eigenheit, aus der sich der
ganze Aufbau des Sammlers ergibt — das Netzwerkmodul startet alle ~102
Sekunden neu und bedient danach nur die ersten ein, zwei Verbindungen.

Dazu die Byte-Lagen samt der einen Abweichung vom ioBroker-Adapter
(wortvertauschte Gesamtzähler), die Aufteilung der 128 Zellen auf vier
Antworten, was unter solarManager/byd/… herauskommt, die beiden Tabellen in
solarLog und wie speicher.js daraus die Ansicht baut.

Dabei aufgefallen und vermerkt: ajax/speicher.php?was=gesundheit hat keinen
Aufrufer. Die Tageswerte für ein Alterungsdiagramm liegen also schon bereit,
es fehlt nur die Anzeige.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-21 12:34:50 +02:00

11 KiB

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

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).

Automatiken

Eigenes Dokument: automatiken.md. 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) 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

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), 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