Im develop Branch sind ein paar größere Neuerungen in lib.db und dem database Plugin gelandet. Der letzte Teil behandelt die aktuellen Updates, die ersten Teile gehen nochmal auf schon fertige Änderungen ein, die aber offensichtlich noch nicht alle erreicht haben 🙂
1. Datenlücken statt „Wert unverändert“
Wenn eine Datenquelle zeitweise ausfällt (offline, Verbindung nicht da) oder keine Daten liefert, behält das Item derzeit einfach seinen letzten Wert. Ohne Korrekturen wird dieser Wert für die gesamte Zeitspanne in der Datenbank gespeichert. Neben verfälschten Berechnungen haben auch Plots/Graphen dabei durchgehende Linien, obwohl ggf. keine Daten vorlagen.
Das ist ein altes Thema, über das in den Issues schon viel diskutiert wurde, und es gibt eine Lösung. Genaugenommen – zwei. item.db_mark_invalid() / item.db_mark_valid().
# Verbindung verloren
sh.solar.leistung.db_mark_invalid(caller='solar_plugin', source='connection_lost')
# Verbindung wieder da
sh.solar.leistung.db_mark_valid(caller='solar_plugin', source='connection_restored')
sh.solar.leistung(neuer_wert, 'solar_plugin')
Wichtig dabei: das passiert nicht von allein. Das jeweilige Plugin muss den Ausfall selbst erkennen und db_mark_invalid() aktiv aufrufen. Unterstützt ein Plugin das (noch) nicht, bleibt der letzte Wert wie bisher einfach unverändert stehen. Aktuell macht das noch kein Plugin von Haus aus – Gateway-Plugins wie MQTT (Verbindung zum Broker verloren) oder KNX (Bus-Timeout, knxd nicht erreichbar) wären die ersten Kandidaten, die ich in absehbarer Zeit angehen möchte. Wer Lust hat, das für sein Lieblings-Plugin nachzurüsten: immer her damit.
Neu und ohne Plugin-Unterstützung nutzbar: das Item-Attribut database_invalid_after. Bekommt ein Item über die angegebene Zeitspanne keine Änderung/kein Update, wird automatisch eine Lücke geöffnet:
solar:
leistung:
type: num
database: init
enforce_updates: true
database_invalid_after: 10m
enforce_updates muss gesetzt sein, sonst zählt ein unverändertes Update nicht als Lebenszeichen. Und: das ist nur für Items sinnvoll, die ohnehin regelmäßig aktualisiert werden (cycle/crontab o.ä.) – bei unregelmäßig/ereignisgesteuert aktualisierten Items gibt’s sonst nur Fehlalarme.
Sobald wieder ein Wert gesendet wurde, wird automatisch db_mark_valid() aufgerufen und die Werte „zählen“ wieder.
Lückeneinträge werden bei avg/sum/integrate/on/min/max automatisch ausgeschlossen, tauchen bei Rohwertabfragen als NULL (aus der Datenbank) auf (für Lücken in Plots), und lassen sich im Webinterface pro Datensatz auch manuell setzen/zurücknehmen.
Beispiel: KNX-Item und MQTT-Item
Zwei Items, zwei Transportwege, aber beide mit demselben eigentlichen Risiko: die Verbindung zur Außenwelt kann abreißen, ohne dass shng selbst oder das jeweilige Plugin davon groß Notiz nimmt – Telegramme bzw. Nachrichten bleiben einfach aus:
heizung:
vorlauftemperatur:
type: num
database: init
knx_dpt: 9
knx_listen: 1/2/3
enforce_updates: true
database_invalid_after: 15m
garage:
aussentemperatur:
type: num
database: init
mqtt_topic_in: garage/aussentemperatur
enforce_updates: true
database_invalid_after: 3m
heizung.vorlauftemperatur kommt vom Sensor selbst zyklisch (in ETS am Gerät eingestellt, z.B. alle 5 Minuten senden, unabhängig von einer Wertänderung) – komplett unabhängig von shngs eigenem Scheduler. Fällt knxd aus oder bricht die Verbindung zum Bus ab, bleiben die Telegramme einfach aus. Das KNX-Plugin selbst muss dabei nicht abstürzen oder für dieses eine Item einen auffälligen Fehler werfen – andere Items mit zwischenzeitlich gecachten Werten sehen auf den ersten Blick unauffällig aus. Genau diesen Fall fängt database_invalid_after ab, ohne dass jemand ins Log schauen muss.
garage.aussentemperatur kommt per MQTT rein, mit demselben Prinzip: viele Sensoren (Zigbee2MQTT, Tasmota, …) senden zyklisch, unabhängig davon ob sich der Wert geändert hat – hier alle 60s angenommen, daher 3m als Schwelle. Bleibt der Sensor tot oder der Broker weg, bleibt auch das MQTT-Update aus, ohne dass shng selbst etwas falsch macht.
Ein rein shng-internes cycle+eval-Item wäre dagegen kein gutes Beispiel: solange shng überhaupt läuft, feuert dessen Scheduler garantiert, und stünde der Scheduler wirklich still, würde auch die Prüfung selbst (invalid_check_cycle) nicht mehr laufen – es gäbe also niemanden mehr, der Alarm schlagen könnte.
Der Haken liegt nicht bei MQTT als Transportweg, sondern bei Quellen, die nur bei Zustandsänderung senden – klassisch Tür-/Fensterkontakte, viele Taster. Da ist „seit 30 Minuten kein Update“ der absolute Normalfall, wenn eben nichts passiert ist, kein Ausfall. database_invalid_after ist nur sinnvoll, wenn die Quelle nachweislich regelmäßig sendet – egal ob per MQTT, KNX oder sonstwie -, und die Schwelle muss zu genau diesem Intervall passen, nicht zu einer geschätzten „sollte doch mal was kommen“-Zeit.
Das Risiko bei Fehlkonfiguration – falscher Itemtyp ohne regelmäßige Updates, oder zu kurz gewählte database_invalid_after-Zeit – sind Zeiträume, die in der Datenbank als „nicht vorhanden“ eingetragen sind, obwohl eigentlich ein gültiger Wert vorlag. Das fällt seinerseits nur auf, wenn man es regelmäßig auf Plots überprüft.
2. Aggregierung statt Löschen – die Optionen im Überblick
database_maxage ist ein bekanntes Item-Attribut und löscht alte Werte ersatzlos. Über database_maxage_action lässt sich jetzt stattdessen ein „Ersatzwert“ zu einem Intervall verdichten:
| Aktion | Bedeutung | Typ |
|---|---|---|
delete |
löschen (Standard) | beliebig |
avg |
zeitgewichteter Mittelwert | num, bool |
sum |
Summe | num, bool |
min / max |
Minimal-/Maximalwert | num, bool |
integrate |
diskretes Integral über der Zeit | num, bool |
duty_cycle (alt: on) |
zeitgewichtete Einschaltdauer | bool |
countall |
Anzahl Rohwerte im Intervall | beliebig |
first / last |
ältester/neuester Rohwert, unverändert | beliebig, auch str |
Das läuft komplett Python-seitig im Plugin, identisch auf SQLite3, MySQL/MariaDB und PostgreSQL. Wer zusätzlich zum Mittelwert auch Min/Max eines Intervalls dauerhaft behalten will, kann dafür die mitgelieferten Structs database.min/database.max nutzen (legen automatisch eigene Kind-Items mit den jeweiligen Werten an).
Beispiel: vier Items, vier verschiedene Konfigurationen
In den Konfigurationen fehlen die eigentlichen Datenquellen, die darf sich jeder dazu denken.
# plugin.yaml
database:
plugin_name: database
driver: sqlite3
default_maxage_interval: 24h
# items/beispiel.yaml
wohnzimmer:
temperatur:
type: num
database: init
database_maxage: 90
database_maxage_action: avg
database_maxage_interval: 1h
pv:
leistung:
type: num
database: init
database_maxage: 365
database_maxage_action: integrate
database_maxage_interval: 1d
haustuer:
kontakt:
type: bool
database: init
database_maxage: 30
database_maxage_action: countall
heizung:
status:
type: str
database: init
database_maxage: 14
database_maxage_action: last
database_maxage_interval: 6h
wohnzimmer.temperatur: nach 90 Tagen werden die Rohwerte je Stunde zu einem Mittelwert verdichtet – Langzeitverlauf bleibt Jahre erhalten, die minütlichen Rohdaten nicht.pv.leistung: nach einem Jahr wird aus der Momentanleistung je Tag die tatsächlich erzeugte Energie integriert – klassischer Fall fürintegrate.haustuer.kontakt: setzt kein eigenesdatabase_maxage_interval– fällt auf den Plugin-Parameterdefault_maxage_interval: 24hzurück. Nach 30 Tagen bleibt pro Tag nur noch die Anzahl der Türöffnungen übrig.heizung.status: einstr-Item – hier funktionierenavg/sum/etc. nicht, aberlastschon. Nach 14 Tagen bleibt je 6h-Intervall nur noch der letzte bekannte Status übrig.
Effekt in der Praxis: Die Kompaktierung läuft im selben Zyklus wie das bisherige Löschen (removeold_cycle) und arbeitet sich – genau wie das – in begrenzten Schritten durch alte Daten (max_aggregate_intervals), pro Item. Bei vier Items mit doch recht unterschiedlichem Alter blockiert also keines der anderen den Durchlauf.
Ein Punkt, den man im Hinterkopf behalten sollte: Ändert man später bei einem Item mit countall (hier haustuer.kontakt) nachträglich das database_maxage_interval, werden schon kompaktierte alte Intervalle beim nächsten Durchlauf mit in ein neues, anders breites Intervall gezogen – für avg/sum/min/max/integrate/duty_cycle rechnerisch unproblematisch, bei countall zählt ein bereits kompaktiertes Intervall dann aber nur noch als „1“ statt als die ursprüngliche Anzahl Rohwerte. Kein Bug, nur eine Ungenauigkeit, die man bei countall + nachträglicher Intervalländerung kennen sollte.
3. PostgreSQL+TimescaleDB als neues Backend
Neben SQLite3 und MySQL/MariaDB unterstützt das Plugin jetzt auch PostgreSQL, optional mit der TimescaleDB-Erweiterung. Die Treiber-Auswahl ist jetzt auch komfortabler: driver akzeptiert neben den echten Modulnamen (psycopg2/psycopg) auch sprechende Namen wie postgres, postgresql, timescale, timescaledb (und mysql/mariadb statt pymysql) – welches Python-Modul installiert ist, wird automatisch erkannt.
database_timescale:
plugin_name: database
driver: timescaledb
connect:
- host:127.0.0.1
- port:5432
- user:shng
- password:shng_password
- database:shng
Was TimescaleDB bringt:
- Hypertables – die
log-Tabelle wird automatisch in Zeitpartitionen (Dateien für einzelne Zeiträume) aufgeteilt, macht zeitbereichsbasierte Abfragen auf großen Tabellen deutlich schneller. - Native Kompression (
timescale_compress: true) – TimescaleDBs eigene Doku nennt 10-20x (90-95% Speicherplatzersparnis) als üblich für Zeitreihendaten; im eigenen Test gegen einen echten 22-Millionen-Zeilen-Datensatz kamen 17,39x raus. - Native Aggregation (
timescale_native_aggregation: true) – statt Python-seitiger Aggregierung übernimmt TimescaleDB das über Continuous Aggregates, direkt auf dem Server. Dieselben Item-Attribute (database_maxage_actionetc.) steuern das weiterhin. - Native Retention (
timescale_native_retention: true) – alte Rohdaten-Zeitpartitionen werden automatisch server-seitig fallengelassen statt Item für Item gelöscht.
Und die Nachteile, ehrlich gesagt:
- Mehr Aufwand als SQLite3 (extra Server, extra Python-Paket
psycopg2-binaryoderpsycopg). - Native Retention ist ein globaler Schwellwert für die ganze Tabelle, kein Wert pro Item mehr – auch Items mit
database_maxage_action: deletewarten dann auf denselben globalen Schwellwert. - Wichtig: Items ohne
database_maxagesind davon nicht ausgenommen. ALLE Items werden nach der längsten eingestellten Zeit aggregiert – oder gelöscht. Wenn keinedefault_maxage_actioneingestellt ist, werden alle nicht ausdrücklich anders konfigurierten Datenbankwerte der Items gelöscht. Wer native Retention nutzt, solltedefault_maxageunddefault_maxage_actionsetzen. - Der Wechsel von nativer Aggregation zurück zu Plugin-Aggregierung ist nicht vorgesehen (nicht implementiert, da alles andere als trivial) und nicht verlustfrei möglich.
- Auch die einmal aktivierte Kompression lässt sich über das Plugin nicht mehr rückgängig machen.
Warum gerade TimescaleDB
Es gibt schon ein InfluxDB-Plugin, und Influx gilt seit Langem als die Referenz für Zeitreihen-Datenbanken. Das Problem mit InfluxDB ist dabei zweierlei: zum Einen hat sich die API mit jeder größeren Version komplett geändert, was ständigen Updateaufwand mit sich bringt und für jede Version ein eigenes Plugin erfordern würde. Zum Anderen ist die aktuelle Open-Source-Linie (InfluxDB 3 Core) explizit als Edge-/Echtzeit-Collector positioniert – Compactor und Optimierungen für Langzeit-Speicherung fehlen, und genau die wären für unseren Stats-/Grafik-Anwendungsfall am wichtigsten. Für brauchbare Abfrage-Performance über die Historie braucht es InfluxDB Enterprise. Es gibt zwar eine kostenlose Hobby-Stufe, aber damit hängt man an einer Hersteller-Lizenzstufe statt an einer eigenständigen Open-Source-Installation – für uns als reines Community-Projekt nicht ideal.
TimescaleDB basiert auf PostgreSQL, einer seit Langem gepflegten, gut getesteten und leistungsfähigen Datenbank, die auf den meisten System problemlos verfügbar ist. Die TimescaleDB-Erweiterung bringt mit der nativen Kompression einen großen Platzvorteil mit (siehe oben, 10-20x laut Timescale, 17,39x im eigenen Test), und die nativen Aggregations- und Retentionsfunktionen entlasten SmartHomeNG von Routineaufgaben, die sonst Python-seitig laufen müssten.
Beispiel: dieselben vier Items, jetzt nativ auf TimescaleDB
An den Items selbst ändert sich nichts – dieselbe Konfiguration wie oben. Nur plugin.yaml bekommt zwei zusätzliche Zeilen:
database_timescale:
plugin_name: database
driver: timescaledb
connect:
- host:127.0.0.1
- port:5432
- user:shng
- password:shng_password
- database:shng
default_maxage_interval: 24h
timescale_hypertable: true
timescale_native_aggregation: true
timescale_native_retention: true
Was jetzt passiert: TimescaleDB legt ein Continuous Aggregate je tatsächlich genutzter Intervallbreite an, nicht eines je Item. Die Continuous Aggregates werden intern als virtuelle Tabelle über der log-Tabelle eingeblendet. Bei den vier Beispiel-Items sind das genau drei: 1h (wohnzimmer.temperatur), 6h (heizung.status) und 1d (pv.leistung und haustuer.kontakt über den default_maxage_interval-Fallback teilen sich hier eines, weil beide auf dieselbe Intervallbreite kommen). Die Aggregation selbst läuft weiter pro Item korrekt nach dessen eigener Konfiguration.
Der eigentliche Unterschied, konkret an diesem Beispiel: pv.leistung hat mit 365 Tagen das mit Abstand längste database_maxage. Unter nativer Retention wird genau dieser Wert (plus eine Zeitpartition Sicherheitsspanne) zum einzigen Schwellwert für die gesamte Tabelle. Die Rohdaten von haustuer.kontakt – eigentlich für 30 Tage konfiguriert – bleiben deswegen faktisch bis zu ein Jahr lang liegen, weil TimescaleDB pro Zeitpartition löscht und diese Item-übergreifend geteilt werden. Im Plugin-Modus (Abschnitt 2) würde haustuer.kontakt dagegen zuverlässig nach 30 Tagen kompaktiert. Das ist genau der „globaler statt Item-genauer Schwellwert“-Nachteil von oben, nur diesmal mit echten Zahlen dahinter. Durch die Kompression schrumpft der Platzbedarf für die eigentlich nicht erforderlichen Daten so weit, dass der zusätzliche Speicherplatz in der Praxis kein Problem darstellt. Trotzdem lässt sich dieses Feature optimal nutzen, wenn database_maxage bei allen Items in derselben Größenordnung liegt.
Für die meisten Home-Installationen reicht SQLite3 grundsätzlich aus. Da ich aber gehört habe, wie viele Millionen Zeilen viele Leute im Forum in ihren Datenbanken haben, wird TimescaleDB schon wieder interessant – und sei es nur aufgrund der Kompression, selbst ohne native Aggregation/Retention.
Migration von Datenbanken
Wer bestehende Daten zwischen Backends umziehen will (z.B. SQLite3 → TimescaleDB, oder umgekehrt): dafür gibt’s jetzt tools/db_migrate.py als eigenständiges Skript (läuft nur bei gestopptem SmartHomeNG). Migriert Item- und Log-Daten direkt zwischen zwei beliebigen unterstützten Backends, unterstützt Resume und --dry-run. Aber vorsicht – selbst auf einem schnellen Rechner sollte man für 22 Millionen Zeilen schon gute drei Stunden reine Transferzeit einplanen.
0 Kommentare