python und rrdtool

  • Hallo Forum,

    ich bitte mal wieder um eure Hilfe.

    Zum Auslesen von meinem neuen smarten Stromzähler habe ich mir ein python3-Script geschrieben.

    Problem ist die Übergabe in die RRD-Datenbank – oder die Bildung der Grafiken mit rrdtool.graph.

    Gemäß dem Befehl rrdtool lastupdate wird auch der letzte Wert wiedergegeben. Mit rrdtool dump bekomme ich aber nicht die Anzahl der Datensätze, die ich erwarten würde. Sind deswegen die erzeugten Auswertungsgrafiken leer?

    Wo liegt der Fehler ??


    Erzeugung der Grafik:

    Grüße
    Andreas

    • Official Post

    Das Script führe ich mit Thonny

    Ah ok, dann also mit Python3. War nur so ein erster Gedanke. Du solltest den Shebang trotzdem in

    Code
    #!/usr/bin/env python3

    ändern und die Zeile# -*- coding: utf-8 -*- darunter (da in Python3 obsolet) weglassen, weil das, wie man sieht, zu falschen Schlussfolgerungen führen kann. ;)

  • Hallo,


    was passt denn jetzt genau mit der Datenbank nicht? Kannst du das eventuell ausführlicher beschreiben? Wie sehen die ausgelesenen Werte aus? Und wie soll der Datenbankinhalt aussehen?


    Zum bisherigen Code: Der Shebang, wie schon erwähnt, sollte 'python3' enthalten.

    Konstanten schreibt man GANZ_GROSS. Man sollte auch auf aussagekräftige Namen achten. 'teil', 'dif' etc. sind nichts sagend und erschweren das lesen. Wenn man Namen durchnummeriert ist es oft sinnvoller eine Datenstruktur wie eine Liste zu verwenden.

    `string=''`ist unnötig. Man erstellt Namen wenn man sie verwendet, abgesehen von Konstanten. Das gilt auch für 'teil', 'zaehlerstand', 'pos', 'start', 'i', 'dif' und 'letzter_wert'.

    Für das Arbeiten mit Pfade gibt es 'pathlib'.

    'try/except' ist nicht als Alternative zu 'if/else' zu verwenden. An dieser Stelle wird der Einsatz von pathlib wieder interessant. Zum überprüfen ob eine Datei vorhanden ist, gibt es das Modul 'exists()', 'is_file' oder für einen Ordner 'is_dir()'.

    Strings verbindet man nicht mit '+' dafür gibt es die format-Methode oder f-Strings.

    if-Bedingungen benötigen keine Klammern. Wenn du die Verbindung mit einem 'with'-Statemant öffnest, wird sie automatisch wieder geschlossen, egal welche Zwischenfälle vorfallen. Dann ist meiner Meinung die Abfrage nach 'isOpen' überflüssig.

    Wenn nach einer 'if'-Bedingung ein 'pass' folgt, sollte man die Bedingung umkehren. In diesem Fall ohne 'not' und das 'else' wird dann überflüssig. Es ist nicht nötig Listen zu leeren, du kannst dem Namen einfach neue Werte zuweisen.

    Wie wird deine Dauerschleife verlassen?

    Willst du 'string' nach der Zählerstandkennung durchsuchen?


    Auf Modulebene sollte kein ausführbarer Code stehen, dort stehen nur Konstanten und Funktionen. Das Programm wird über die Funktion 'main' gesteuert und alles was eine Funktion benötigt bekommt sie als Argumente übergeben, Konstanten sind die Ausnahme.

    Da ich keine Ahnung habe, wie deine Daten ankommen und was du nachher genau haben willst, habe ich mal etwas geraten. Deine Formatierung habe ich so gelassen, da ich den Ausgangszustand nicht kenne.

    Dann sollte später mal so etwas in der Art rauskommen:



    Der Code funktioniert so wahrscheinlich noch nicht, aber darauf kannst du aufbauen.


    Grüße

    Dennis

    ... ob's hinterm Horizont wirklich so weit runter geht oder ob die Welt vielleicht doch gar keine Scheibe ist?

  • Danke Dennis für dein "Rohscript".

    Mein Fehler lag aber nicht in dem python-script, sondern in einer falsche RRD-Einstellung.

    Mir war bis eben nicht klar, dass in der ersten Zeile mit „RRA“ nicht das Archiv betrifft, sondern die Anzahl der unveränderlich abgespeicherten Werte.

    "RRA:AVERAGE:0.5:1:2000", besagt: speichere 2000 Werte unverändert, bilde den Mittelwert (AVERAGE) ab dem 2001ten Datensatz.

    Bevor ich das angepasste Script hier poste, eine Rückmeldung zu deinem Entwurf:

    Code
      File "/home/pi/Desktop/strom2.py", line 30, in read_sensor
        with open(serial_port):
    TypeError: expected str, bytes or os.PathLike object, not Serial

    Grüße
    Andreas

  • Hiho;


    Frage: rrdtool braucht doch für das updaten die genaue syntax:

    Code
    rrdtool update demo.rrd N:3.44

    -> trägt den Wert 3.44 für den aktuellen Zeitpunkt ein.


    -Was bewirkt in Zeile 51 das 'N : %s' ?


    Dann:

    rrdtool akzeptiert bei mir keine float- oder integer-Werte, sondern nur strings.


    Deine gezeigte Database hat jedenfalls nur eine Zeile (die 59), eigentlich sollten da eben viele erzeugt werden.

    Also mal versuchen, das j mit

    Code
     j=str(int(dif*60/10))

    umzuwurschteln.

  • Hallo rasray,
    rrd spechert generell nur Zeichenfolgen (=String). Daher müssen alle Werte mit str(zahl) umgewandelt werden.
    Ich hoffe, es richtig zu erklären: 'N: %s' : Das "N:" ist ein Art Schlüselwort, "%s" besagt, der Inhalt {hier (j)} ist als string formatiert.

  • Hallo, AndreasO;

    wieder was gelernt, das N: war mir klar, das erzwungene stringformat kannte Ich so noch nicht, deshalb arbeitete Ich bisher wie beschrieben.


    Zu #6:

    Jou, da Du in allen rra's da "1" drin hattest, erzeugt rrd EINE Zeile, die dann wieder überschrieben wird -> keine Grafik erzeugbar.


    Wichtig ist immer dein step: 60 Sekunden


    "RRA:AVERAGE:0.5:1:2000":

    Die "1" bedeutet hier, dass jeder Einzelwert ohne Mittelung gespeichert wird.

    Die "2000" bedeutet, dass davon 2000 gespeichert werden, der 2001te überschreibt dann den Ältesten.

    Bei einem step von 60 kannst Du also in einer Grafik auf die letzten 120.000 Sekunden als Einzelwerte zurückgreifen.


    Mittelwerte:

    Deine zweite rra hattest Du mit "10" angelegt.

    Also wird hier jeweils aus 10 steps ein Mittelwert gebildet

    -> Alle 600 Sekunden = 10 Minuten. Auch hier gibt die letzte Zahl an, wieviele dieser Zeilen gespeichert bleiben sollen.

    Bei "144" bleiben diese 10-Minuten-Mittelwerte also für einen Tag verfügbar. (60*10*144 = 86400 Sekunden = 24 Stunden)


    Eine 3. RRA:AVERAGE:0.5:1440:3600 bildet also einen Mittelwert über einen ganzen Tag (1440*60 Sekunden), speichert davon 3600 ab, reicht also für 10 Jahre, erst dann würde der älteste dieser Tages-Mittelwert überschrieben.

  • Ich nochmal:

    Hatte ewig nicht mehr in die Dokumentation von rrdtool reingeschaut: Es ist mittlerweile einfacher geworden, diese Intervalle anzugeben:

    Code
    rrdtool create subdata.rrd -s 10 
    DS:ds0:GAUGE:5m:0:U
    RRA:AVERAGE:0.5:5m:300h
    RRA:AVERAGE:0.5:15m:300h
    RRA:AVERAGE:0.5:1h:50d

    Das alte Sekunden-in-Stunden-Tage-Wochen-Jahre umrechnen war ja sehr unübersichtlich.

    Jetzt zB die zweite RRA:

    Es wird ein 15-Minuten-Mittelwert gebildet, dieser bleibt 300 Stunden gespeichert, dann überschreibt rrdtool diesen Wert.

  • Es ist geschafft: Script läuft, und läuft, und läuft, und......

    Bestimm kann das eine oder andere effiziente programmiert werden. Für meine Zwecke reicht es.

  • AndreasO: `base64` und `datetime` werden importiert, aber nicht verwendet. Wobei `datetime` gar keine so schlechte Idee wäre, weil aktuelles Datum plus Zeit damit ein bisschen einfacher ausgegeben werden können als über `time.strftime()` und `time.localtime()`.


    Pfadteile setzt man nicht mit Zeichenkettenoperationen zusammen. Dafür gibt es `pathlib` was Du ja auch schon teilweise verwendest.


    Namen sollten keine kryptischen Abkürzungen enthalten oder gar nur daraus bestehen und keine Grunddatentypen enthalten. Also beispielsweise der `fu_`-Präfix bei den Funktionen gehört da nicht hin. Funktionen (und Methoden) sind üblicherweise nach der Tätigkeit benannt die sie durchführen. Dann weiss der Leser was die tun, und kann sie von eher passiven Werten unterscheiden, auch ohne einen kryptischen Präfix.


    `fu_search_database()` sucht nichts, sondern legt an falls noch nicht vorhanden. Oder anders ausgeedrückt, stellt sicher das die Datenbank existiert: `ensure_database_exists()`.


    Das Argument von `fu_read_sensor()` wird überhaupt gar nicht verwendet, dann braucht man das auch nicht zu übergeben und `wert` in der Hauptfunktion fällt weg.


    Der Test auf `isOpen` ist überflüssig. Wenn man beim Erstellen des `Serial`-Objekts einen Port angegeben hat, dann *ist* der offen. Andernfalls hätte das Erstellen zu einer Ausnahme geführt.


    Den seriellen Port sollte man auch sauber wieder schliessen. Am sichersten geht das mit der ``with``-Anweisung.


    Die 0 die am Anfang an `dif` gebunden wird, wird nirgends verwendet, das sollte man sich also sparen. Ausserdem sollte man Namen nicht so weit von dem Ort einführen an dem sie dann letztendlich verwendet werden.


    `schleife` ist kein guter Name für ein Flag und statt 0 und 1 sollte man `False` und `True` für Wahrheitswerte verwenden. Die Variable kann aber sowieso weg, weil man das was damit ”bewacht” wird, damit es nur beim ersten Schleifendurchlauf passiert, auch einfach *vor* die Schleife ziehen kann.


    Die Namen aus `serial` die nicht den Namenskonventionen entsprechen sollte man nicht mehr verwenden. `flushInput()` heisst jetzt `reset_input_buffer()`. Ich sehe aber nicht wozu das bei einer seriellen Verbindung die gerade erst geöffnet wurde, gut sein soll‽


    Das mit dem ``pass`` als einzige Anweisung in einem ``if``-Zweig wurde ja bereits von Dennis89 erwähnt.


    Die Verarbeitung der Daten von der seriellen Schnittstelle ist ziemlich kaputt. Offensichtlich kommen da Binärdaten rein, womit `readline()` schon mal falsch ist, denn Zeilenenden gibt es in dem binären Datenstrom nicht. Das funktioniert auch nur, weil Du gleichzeitig ein `timeout` gesetzt hast, so dass nach spätestens einer Sekunde alles geliefert wird, was bis dahin angekommen ist. Es kann also sein, dass zufällig ein Bytewert 0x0A in den Daten vorkommt, oder das in der Sekunde nur ein Teil von dem Wert angekommen ist, der Dich interessiert.


    Das nächste Problem ist dann die Umwandlung in eine Zeichenkette mit der Hexadezimaldarstellung. Das sind dann ja zwei Zeichen pro Byte beziehungsweise ein Zeichen pro 4 Bit. Wenn Du da dann eine andere Zeichenkette mit derart kodierten Bytewerten drin suchst, suchst Du nicht nur in den Originaldaten, sondern auch in den Originaldaten die um 4 Bit verschoben sind. Ein `split()` mit "ff56" als Trenner würde auch "1ff650" trennen, obwohl in den Originaldaten die Bytefolge 0xff 0x56 gar nicht vor kommt. Das gilt auch für die längere Kennzahl, auch wenn es dort natürlich unwahrscheinlicher ist, das dieser Wert um vier Bit verschonen in den Daten vorkommt.


    Das ist alles wenig robust und ziemlich undurchsichtig.


    Ein bisschen besser wäre es bei den Binärdaten zu bleiben, und erst bis zu der Kennzahl zu lesen, dann bis zu dem Präfix für den Wert, und von da dann die nächsten fünf Bytes, und die in eine ganze Zahl umwandlen.


    Wenn man das so macht ist in der ``while``-Schleife kein ``if`` mehr und das ``break`` auf unbedingt die letzte Anweisung in der Schleife, womit das gar keine Schleife mehr ist, sondern einfach nur noch genau einmal linear ausgeführter Code.


    Die Reihenfolge der Berechnung von `anzeige` und `zaehler_rrd` ist unverständlicher als sie sein müsste. Wenn erst `zaehler_rrd` ausrechnen würde (in Wh) und *daraus* dann `anzeige` (in kWh), hätte man einen Teiler von 1.000, was bei Wh nach kWh logisch ist, und keinen Teiler von 10.000, der ”komisch” ist.


    Die `float()`-Aufrufe in der Funktion machen keinen Sinn. Das sind jeweils bereits (Gleitkomma)Zahlen.


    `fu_letzter_wert()` ist bei den Namen echt schlimm. Alles einbuchstabige lokale Namen, nur drei Stück, dafür wird `b` für verschiedene Werte und Typen immer wiederverwendet und `l` wird definiert, mit einem Wert dessen Sinn sich mir nicht ganz erschliesst, und dann überhaupt nicht verwendet.


    Und die ”Schleife” ist total sinnfrei. Die wird genau ein einziges mal durchlaufen. Damit ist das keine Schleife. Was soll das?


    Verwende keine nackten ``except:``\s ohne konkrete Ausnahmen. Damit werden *alle* Ausnahmen behandelt, auch solche mit denen man nicht rechnet. In der Funktion würde das dann einfach verschluckt und die liefert eine 0, ohne das Du mitbekommst, dass da etwas unerwartetes passiert ist. Du wandelst eine Zeichenkette in eine Gleitkommazahl um, dabei kann es einen `ValueError` geben, und auch nur den solltest Du behandeln.


    0 ist ein schlechter Fehlerwert, weil das gleichzeitig auch ein gültiger Zählerstand sein kann. Für ”nichts” gibt es den Wert `None`.


    ``str(0)`` ist eine komische Umschreibung von "0". Wobei `dif` ja eigentlich eine Zahl ist, das sollte dann auch vom Typ her tatsächlich eine Zahl sein und keine Zeichenkette.


    Bei den Rechnungen bin ich auch ein bisschen verwirrt: Wenn die Zählerstände in Wh vorliegen wieso sollte dann bei der Differenz plötzlich Ws heraus kommen?


    `fu_read_sensor()` macht deutlich zu viel. Der Name vermittelt, dass dort der Sensor ausgelesen wird. Tatsächlich wird der Sensor ausgelesen, die Differenz zum letzten Wert ermittelt, beides in die Datenbank geschrieben, *und* dann auch noch ein Bild erstellt. Ausser dem ersten Punkt gehört das da alles nicht rein.


    `fu_plotten()` *muss* man als Argument die Zeichenkette "verbrauch" übergeben. Jeder andere Wert führt zu einer Ausnahme. Damit ist dieses Argument ziemlich unsinnig.


    Die ``if``/``elif``-Kaskade die aus "2" "2h", aus "6" "6h", … macht ist einfach nur ein Anhängen von "h" an den Wert von `plot`.


    Zwischenstand (ungetestet):

    Das Verarbeiten der Sensordaten könnte man robuster machen, denn das SML-Protokoll hat ja eine bekannte Struktur.

    Who is General Failure and why is he reading my hard disk?

  • Hallo,


    __blackjack__ gibt es einen Grund für das Umbenennen von 'datetime' in 'DateTime'? Abgesehen von persönlichem Geschmack? Machst du das um 'datetime' von 'datetime' zu unterscheiden?


    P.S. Hast du dir mal überlegt ein Lehrbuch zu schreiben? :geek:



    Grüße

    Dennis

    ... ob's hinterm Horizont wirklich so weit runter geht oder ob die Welt vielleicht doch gar keine Scheibe ist?