Vorwort
Aufgrund eines Feedbacks zu meiner SML-Seite möchte ich hier jetzt ein paar Worte vorwegschicken.- Die hier beschriebenen Python Scripte wurden mit der (veralteten) Version 2.7 erstellt.
Das Portieren auf eine aktuelle Version habe ich nicht vor, und kann dem, der es vor hat, einige Mühe bereiten. - Die Scripte sind relativ simpel programmiert, was für meine Zähler allerdings ausreichend war. Für andere Zähler können erhebliche Änderungen notwendig werden, damit sie weiter vernünftige Werte ausgeben.
- Vermutlich sind auch Änderungen notwendig, wenn eingespeißt werden sollte. -> Stichwort Zweierkomplement
- Es gibt Zähler mit dynamischen Längen der Listeneinträge, auch das beherrschen meine Scripte nicht!
- Mir geht es bei dem Beitrag auf dieser Seite nicht darum eine Universallösung für alle möglichen Zählertypen zu präsentieren. Sondern lediglich nur darum zu zeigen, welche ganz eigenen Probleme ich in diesem Projekt gelöst habe.
- Für fertige Lösungen einfach mal "Volkszaehler" googeln.
Ab hier geht die Projektbeschreibung los
Bei einem Blick in meinen Stromzählerschrank stellte ich fest, dass die beiden darin befindlichen Zähler jeweils eine optische Schnittstelle besitzen. Der erste Zähler ist vom Typ: "eHZ" genauer: "eHZ-GW8E2A500AK1" von EMH (Wärmepumpe).Der zweite Zähler ist vom Typ: "MT175" genauer: "MT175-D1A52-V22-K0t" von ISKRA (allgemeiner Bedarf)
Das Interesse war also geweckt. Ich hoffte über diese Schnittstelle den aktuellen Zählerstand, auch aus der Ferne ablesen zu können. Nach etwas Suche im Netz fand ich heraus, dass es so genannte SML-Schnittstellen sind (SML = Smart Message Language). Meine Erwartungen an die Schnittstelle wurden sogar noch übertroffen. So ist nicht nur der Zählerstand abrufbar, sondern zusätzlich noch diverse andere Parameter.
Dazu später mehr, zunächst beschreibe ich das Herstellen des Interfaces.
Die Schnittstelle
Die spätere Auswertung der Daten sollte ein Raspberry Pi übernehmen, daher habe ich die Schnittstellenplatine rechnerseitig mit einer USB-Schnittstelle konzipiert. Auf der Zählerseite kommt eine Infrarotschnittstelle zum Einsatz. Die Angaben zu den verwendeten Wellenlängen schwanken von 800nm bis 1000nm. In diesem Projekt wird als Empfänger ein Phototransistor vom Typ: "SFH 309 FA-4" verwendet. (die "4" ist ein Maß für die Empfindlichkeit bzw. des Photostromes bei 950nm)Um optische Rückkopplungen zu vermeiden, sollte eine Sendediode mit einem recht geringen Öffnungswinkel genutzt werden.
Hier kommt eine 3mm LED vom Typ: "SEP8705-003" zum Einsatz (Öffnungswinkel 15° und Wellenlänge 880nm).
der Schaltplan
Das Herzstück der Schaltung bildet ein FT232RL von FTDI. Wird dieses IC an eine freie USB-Schnittstelle eines Rechners angeschlossen, stellt es eine virtuelle serielle Schnittstelle bereit. Je nach Beschaltung können die TTL-Pegel 5V oder 3,3V betragen. In diesem Projekt werden 5V Pegel verwendet. Das IC und die restliche Peripherie werden direkt vom PC mit Spannung versorgt. Die LEDs "RX" und "TX" zeigen an, ob gerade Daten gesendet oder empfangen werden. Im oberen rechten Bereich des Bildes ist der Sender dargestellt. Der Transistor T2 arbeitet hier als Verstärker und gleichzeitig als notwendiger Invertierer des Signals vom FTDI-Chip. R4 begrenzt den Vorwärtsstrom der Sendediode. R5 dient als Basisvorwiderstand. Darunter ist der Empfanger abgebildet: Über R2 wird das Signal des Zählers abgenommen. T1 in Verbindung mit R3 arbeiten wieder als Invertierer. Um steile Flanken zu garantieren, ist noch ein Schmitt-Trigger nachgeschaltet.Ja aber, da fehlt doch die im Datenblatt genannte Ferritperle!!! JA, ich weiß - und es geht auch ohne.
Löten
Das Löten ging aufgrund der wenigen Bauteile schnell von der Hand.das Gehäuse
Die Form des Gehäuses ist mehr oder weniger durch die Zähler vorgegeben. Die Hersteller der Stromzähler haben an der Schnittstelle netterweise eine Metallplatte vorgesehen. So lässt sich das Gehäuse einfach per Ringmagnet befestigen. Die CAD-Dateien habe ich mit dem Programm "123D Design" erstellt und als *.stl Dateien exportiert. Diese Dateien können wiederum mit einem so genannten Slicer für 3D-Drucker aufbereitet werden. In der Graphik unten sind die stl-Dateien für das Grundgehäuse sowie den Deckel visualisiert.Falls jemand den Deckel transparent drucken möchte, gibt es ihn auch ohne Durchbruch für die RX/TX Leds.
Viewstl.com is licensed under the MIT License - Copyright © 2010-2018 Viewstl.com authors
three.js is licensed under the MIT License - Copyright © 2010-2018 three.js authors
So sieht das fertige Gehäuse aus:
und im Schrank:
Stückliste
Bauteil | Bestellnummer | Menge | Einzelpreis | Gesamtpreis |
---|---|---|---|---|
TME | ||||
Fototransistor | SFH309FA-4 | 1 | 0,62 € | 0,62 € |
Schmitt-Trigger | SN74LVC1G17DBV | 1 | 0,10 € | 0,10 € |
Knickschutztülle | H125-HV-BK-M1 | 1 | 0,17 € | 0,17 € |
IR-Sendediode alternative | OSI3CA3131A | 1 | 0,13 € | 0,13 € |
Reichelt | ||||
IR-Sendediode abgekündigt!!! | SEP 8705-003 | 1 | 1,50 € | 1,50 € |
USB IC | FT 232 RL | 1 | 3,60 € | 3,60 € |
npn-Transistor | BC846 | 1 | 0,04 € | 0,04 € |
pnp-Transistor | BCX17 | 1 | 0,05 € | 0,05 € |
Widerstand 0805 270R R4 für SEP 8705-003 | SMD-0805 270 | 1 | 0,03 € | 0,03 € |
Widerstand 0805 1K R4 für OSI3CA3131A | SMD-0805 1,00K | 1 | 0,03 € | 0,03 € |
Widerstand 0805 390R | SMD-0805 390 | 2 | 0,03 € | 0,06 € |
Widerstand 0805 12k | SMD-0805 12,0K | 3 | 0,03 € | 0,09 € |
Kerko 0805 100n | X7R-G0805 100N | 2 | 0,03 € | 0,06 € |
Kondensator Tantal 22μF | SMD TAN .22/20 | 1 | 0,26 € | 0,26 € |
USB Kabel | AK 670/2-3,0 | 1 | 1,35 € | 1,35 € |
Voelkner / Conrad | ||||
SMD LED 1206 rot | 156314 | 1 | 0,20 € | 0,20 € |
SMD LED 1206 grün | 156315 | 1 | 0,20 € | 0,20 € |
Supermagnete.de | ||||
Ringmagnet | R-27-16-05-N | 1 | 2,81 € | 2,81 € |
Summe: | 11,14 € | |||
Summe: | 9,77 € |
Dazu kommen noch die Kosten für die Platine und das Gehäuse.
Das SML Protokoll
Sucht man Informationen zum SML-Protokoll findet man recht schnell einen Verweis zum Bundesamt für Sicherheit in der Informationstechnik (BSI). In der technischen Richtlinie BSI TR-03109-1 Anlage IV: Feinspezifikation "Drahtgebundene LMN-Schnittstelle" Teil b: "SML - Smart Message Language" ist das Protokoll beschrieben. Damit allein kommt man noch nicht recht weit, das Zauberwort heißt dann "OBIS-Kennzahlen". Mit Hilfe dieser Kennzahlen kann man heraus finden, welche Arten von Daten übertragen werden. (z.B. Wirkleistung, Momentanleistung, Arbeit, usw.)Ein komplettes Telegram (in der Doku des BSI auch Datei genannt) kann aus mehreren SML-Nachrichten bestehen.
Bei meinen beiden Zählern besteht ein Telegram immer aus den drei folgenden SML-Nachrichten:
- getOpenResponse
- getListResponse
- getCloseResponse
Etwas häufiger als alle 2 Sekunden senden die Zähler ein komplettes Telegram. Bei mir mit einer Datenrate von 9600Baud und den Settings 8-N-1. Damit dieser Datenstrom für einen Menschen "lesbarer" wird, sollte man sich diesen Hexadezimal anzeigen lassen. Je nach Zähler kann Art und Inhalt der Telegramme abweichen. Bei meinem MT175 Zähler war es möglich, über die Eingabe eines Codes zusätzliche Informationen ausgeben zu lassen.
(Dieser muss beim Messstellenbetreiber angefragt werden.)
Schaut man sich den Datenstrom an, sieht er z.B. so aus:
(vom MT175 Zähler - private Daten wurden unkenntlich gemacht)
1b 1b 1b 1b 01 01 01 01 76 05 03 2b 18 0f 62 00 62 00 72 63 01 01 76 01 01 05 01 0e 5d 5b 0b XX XX XX XX XX XX XX XX XX XX 01 01 63 49 00 00 76 05 03 2b 18 10 62 00 62 00 72 63 07 01 77 01 0b XX XX XX XX XX XX XX XX XX XX 07 01 00 62 0a ff ff 72 62 01 65 02 1a 58 7f 7d 77 07 81 81 c7 82 03 ff 01 01 01 01 04 49 53 4b 01 77 07 01 00 00 00 09 ff 01 01 01 01 0b XX XX XX XX XX XX XX XX XX XX 01 77 07 01 00 01 08 00 ff 65 00 01 01 80 01 62 1e 52 ff 59 00 00 00 00 01 c9 0c a7 01 77 07 01 00 01 08 01 ff 01 01 62 1e 52 ff 59 00 00 00 00 01 c9 0c a7 01 77 07 01 00 01 08 02 ff 01 01 62 1e 52 ff 59 00 00 00 00 00 00 00 00 01 77 07 01 00 02 08 00 ff 01 01 62 1e 52 ff 59 00 00 00 00 00 00 00 00 01 77 07 01 00 02 08 01 ff 01 01 62 1e 52 ff 59 00 00 00 00 00 00 00 00 01 77 07 01 00 02 08 02 ff 01 01 62 1e 52 ff 59 00 00 00 00 00 00 00 00 01 77 07 01 00 10 07 00 ff 01 01 62 1b 52 00 55 00 00 00 5b 01 77 07 01 00 24 07 00 ff 01 01 62 1b 52 00 55 00 00 00 17 01 77 07 01 00 38 07 00 ff 01 01 62 1b 52 00 55 00 00 00 2e 01 77 07 01 00 4c 07 00 ff 01 01 62 1b 52 00 55 00 00 00 15 01 77 07 81 81 c7 82 05 ff 01 01 01 01 83 02 XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX 01 01 01 63 54 86 00 76 05 03 2b 18 11 62 00 62 00 72 63 02 01 71 01 63 fa 36 00 1b 1b 1b 1b 1a 00 70 b2
Da der Datenstrom ohne Punkt und Komma (und ohne Zeilenumbrüche o.ä.) gesendet wird, sieht das zunächst etwas unübersichtlich aus. Aber mit nur wenigen Informationen kann man es deutlich besser lesen. Dazu habe ich den Stream in Zeilen unterteilt und mit Tabs unterschiedlich tief eingerückt.
Jedes Telegram beginnt mit folgender Sequenz:
1b 1b 1b 1b #Escape Sequenz 01 01 01 01 #Start Version 1.0
Jetzt muss man nur noch wissen, dass die SML Nachricht in einer Listenstruktur aufgebaut ist.
Das folgende Byte ist Nibble-weise auszuwerten. Die "7" bedeutet, dass es sich um eine Liste handelt, die "6" gibt die Anzahl der Einträge an.
76 #7=Liste 6=Anzahl der Einträge
Nun wissen wir also, dass nach diesem Byte noch 6 Einträge folgen. Das erste Byte jedes Listeneintrages (genauer das niederwertige Nibble) gibt die Eintragslänge (in Byte) an (Das erste Byte zählt mit). Wie man sieht, kann ein Eintrag auch eine weitere Liste enthalten (z.B. in Zeile 7 -> 4. Eintrag in Liste 1). Die Verschachtelung kann dabei beliebig tief sein.
05 03 2b 18 0f #0=Octet String 5=Byte Länge -> 1. Eintrag in Liste 1 62 00 #2. Eintrag in Liste 1 62 00 #3. Eintrag in Liste 1 72 #4. Eintrag in Liste 1 -> neue Liste 2 63 01 01 #1. Eintrag in Liste 2 76 #2. Eintrag in Liste 2 -> neue Liste 3 01 #1. Eintrag in Liste 3 01 #2. Eintrag in Liste 3 05 01 0e 5d 5b #3. Eintrag in Liste 3 0b XX XX XX XX XX XX XX XX XX XX #4. Eintrag in Liste 3 01 #5. Eintrag in Liste 3 01 #6. Eintrag in Liste 3 63 49 00 #5. Eintrag in Liste 1 00 #6. Eintrag in Liste 1
Nun das gesamte Telegram mit Erklärungen:
Eine Besonderheit stellt hier die Zeile 134 dar. Wenn nur ein Nibble für die Längenangabe eines Eintrages genutzt werden könnte, wäre lediglich eine maximale Länge von 15Byte pro Eintrag möglich. Ist das MSB des ersten Nibbles jedoch gesetzt, (also z.B. 1000(Bin) = 8(Hex)) wird nicht nur das zweite Nibble des ersten Bytes, sondern zusätzlich das zweite Byte des Eintrages zur Längenangabe herangezogen.
3(Hex)=11(Bin)
02(Hex)=0010(Bin)
zusammengeschrieben:
11|0010(Bin)=50(Dez)
Also ist der Eintrag in Zeile 134 inkl. der beiden Bytes für die Längenangabe 50 Bytes lang.
weiterer Sonderfall - überlange Listen
Einen ähnlichen Sonderfall gibt es auch bei der Listenankündigung - allerdings nicht bei meinen Zählern.
Bei Listen mit mehr als 15 Einträgen ist das erste Nibble der Listenankündigung nicht 7 sondern f.
Auch hier wird dann zusätzlich das folgende Byte zur Berechnung der Anzahl der Listeneinträge verwendet.
Bsp.: f1 0e
1(Hex)=1(Bin)
0e(Hex)=1110(Bin)
zusammengeschrieben:
1|1110(Bin)=30(Dez)
Diese Liste hätte also 30 Einträge!!!
1b 1b 1b 1b #Escape Sequenz 01 01 01 01 #Start Version 1.0 76 #Liste mit 6 Einträgen (1. SML Nachricht in diesem Telegramm) 05 03 2b 18 0f #transactionId: 62 00 #groupNo: 62 00 #abortOnError: 72 #messageBody: Liste mit 2 Einträgen 63 01 01 #getOpenResponse: 76 #Liste mit 6 Einträgen 01 #codepage: ohne Wert 01 #clientId: ohne Wert 05 01 0e 5d 5b #reqFileId: 0b XX XX XX XX XX XX XX XX XX XX #serverID: (0b(HEX)=11Bytes Länge) 01 #refTime: ohne Wert 01 #smlVersion: ohne Wert 63 49 00 #CRC Prüfsumme der ersten Nahricht 00 #Ende der ersten SML Nachricht 76 #Liste mit 6 Einträgen (2. SML Nachricht in diesem Telegramm) 05 03 2b 18 10 #transactionId: 62 00 #groupNo: 62 00 #abortOnError: 72 #messageBody: Liste mit 2 Einträgen 63 07 01 #getListResponse: 77 #Liste mit 7 Einträgen 01 #clientId: ohne Wert 0b XX XX XX XX XX XX XX XX XX XX #serverID: 07 01 00 62 0a ff ff #ListName: 72 #Liste mit 2 Einträgen 62 01 #choice: secIndex 65 02 1a 58 7f #secIndex: (Uptime) 7d #Liste mit 11 Einträgen 77 #Liste mit 7 Einträgen 07 81 81 c7 82 03 ff #objName: OBIS Kennzahl für den Hersteller 01 #status: ohne Wert 01 #valTime: ohne Wert 01 #unit: ohne Wert 01 #scaler: ohne Wert 04 49 53 4b #value: (49 53 4b)Ascii = ISK 01 #valueSignature: ohne Wert 77 #Liste mit 7 Einträgen 07 01 00 00 00 09 ff #objName: OBIS Kennzahl für Gerätenummer = ServerID 01 #status: ohne Wert 01 #valTime: ohne Wert 01 #unit: ohne Wert 01 #scaler: ohne Wert 0b XX XX XX XX XX XX XX XX XX XX #value: Wert für Gerätenummer 01 #valueSignature: ohne Wert 77 #Liste mit 7 Einträgen 07 01 00 01 08 00 ff #objName: OBIS Kennzahl für Wirkenergie Bezug gesamt tariflos 65 00 01 01 80 #status ? 01 #valTime: ohne Wert 62 1e #unit: 1e = "Wh" 52 ff #scaler: ff(integer)=-1(dezimal) -> 10^-1 = 0.1 59 00 00 00 00 01 c9 0c a7 #unit: Wert für Wirkenergie Bezug gesamt tariflos 01 #valueSignature: ohne Wert 77 #Liste mit 7 Einträgen 07 01 00 01 08 01 ff #objName: OBIS-Kennzahl für Wirkenergie Bezug Tarif1 01 #status: ohne Wert 01 #valTime: ohne Wert 62 1e #unit: 1e = "Wh" 52 ff #scaler: ff(integer)=-1(dezimal) -> 10^-1 = 0.1 59 00 00 00 00 01 c9 0c a7 #unit: Wert für für Wirkenergie Bezug Tarif1 01 #valueSignature: ohne Wert 77 #Liste mit 7 Einträgen 07 01 00 01 08 02 ff #objName: OBIS-Kennzahl für Wirkenergie Bezug Tarif2 01 #status: ohne Wert 01 #valTime: ohne Wert 62 1e #unit: 1e = "Wh" 52 ff #scaler: ff(integer)=-1(dezimal) -> 10^-1 = 0.1 59 00 00 00 00 00 00 00 00 #value: Wert für Wirkenergie Bezug Tarif2 01 #valueSignature: ohne Wert 77 #Liste mit 7 Einträgen 07 01 00 02 08 00 ff #objName: OBIS-Kennzahl für Wirkenergie Einspeisung gesamt tariflos 01 #status: ohne Wert 01 #valTime: ohne Wert 62 1e #unit: 1e = "Wh" 52 ff #scaler: ff(integer)=-1(dezimal) -> 10^-1 = 0.1 59 00 00 00 00 00 00 00 00 #value: Wert für Wirkenergie Einspeisung gesamt tariflos 01 #valueSignature: ohne Wert 77 #Liste mit 7 Einträgen 07 01 00 02 08 01 ff #objName: OBIS-Kennzahl für Wirkenergie Einspeisung Tarif1 01 #status: ohne Wert 01 #valTime: ohne Wert 62 1e #unit: 1e = "Wh" 52 ff #scaler: ff(integer)=-1(dezimal) -> 10^-1 = 0.1 59 00 00 00 00 00 00 00 00 #value: Wert für Wirkenergie Einspeisung Tarif1 01 #valueSignature: ohne Wert 77 #Liste mit 7 Einträgen 07 01 00 02 08 02 ff #objName: OBIS-Kennzahl für Wirkenergie Einspeisung Tarif2 01 #status: ohne Wert 01 #valTime: hier ohne Wert 62 1e #unit: 1e = "Wh" 52 ff #scaler: ff(integer)=-1(dezimal) -> 10^-1 = 0.1 59 00 00 00 00 00 00 00 00 #value: Wert für Wirkenergie Einspeisung Tarif2 01 #valueSignature: ohne Wert 77 #Liste mit 7 Einträgen 07 01 00 10 07 00 ff #objName: OBIS-Kennzahl für momentane Gesamtwirkleistung 01 #status: ohne Wert 01 #valTime: ohne Wert 62 1b #unit: 1b = "W" 52 00 #scaler: 0(integer)=0(dezimal) -> 10^0 = 1 55 00 00 00 5b #value: Wert für momentane Gesamtwirkleistung 01 #valueSignature: ohne Wert 77 #Liste mit 7 Einträgen 07 01 00 24 07 00 ff #objName: OBIS-Kennzahl für momentane Wirkleistung in Phase L1 01 #status: ohne Wert 01 #valTime: ohne Wert 62 1b #unit: 1b = "W" 52 00 #scaler: 0(integer)=0(dezimal) -> 10^0 = 1 55 00 00 00 17 #value: Wert für momentane Wirkleistung in Phase L1 01 #valueSignature: ohne Wert 77 #Liste mit 7 Einträgen 07 01 00 38 07 00 ff #objName: OBIS-Kennzahl für momentane Wirkleistung in Phase L2 01 #status: ohne Wert 01 #valTime: ohne Wert 62 1b #unit: 1b = "W" 52 00 #scaler: 0(integer)=0(dezimal) -> 10^0 = 1 55 00 00 00 2e #value: Wert für momentane Wirkleistung in Phase L2 01 #valueSignature: ohne Wert 77 #Liste mit 7 Einträgen 07 01 00 4c 07 00 ff #objName: OBIS-Kennzahl für momentane Wirkleistung in Phase L3 01 #status: ohne Wert 01 #valTime: ohne Wert 62 1b #unit: 1b = "W" 52 00 #scaler: 0(integer)=0(dezimal) -> 10^0 = 1 55 00 00 00 15 #value: Wert für momentane Wirkleistung in Phase L2 01 #valueSignature: ohne Wert 77 #Liste mit 7 Einträgen 07 81 81 c7 82 05 ff #objName: OBIS-Kennzahl für publicKey 01 #status: ohne Wert 01 #valTime: ohne Wert 01 #unit: ohne Wert 01 #scaler: ohne Wert 83 02 XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX #value: Wert für publicKey 01 #valueSignature: ohne Wert 01 #listSignature: ohne Wert 01 #actGatewayTime: ohne Wert 63 54 86 #CRC Prüfsumme der zweiten Nachricht 00 #Ende der zweiten SML Nachricht 76 #7=Liste 6=Anzahl der Einträge (3. SML Nachricht in diesem Telegramm) 05 03 2b 18 11 #transactionId: 62 00 #groupNo: 62 00 #abortOnError: 72 #messageBody: Liste mit 2 Einträgen 63 02 01 #getCloseResponse: 71 #Liste mit einem Eintrag 01 #? ohne Wert 63 fa 36 #CRC Prüfsumme der dritten Nachricht 00 #Ende der dritten SML Nachricht 1b 1b 1b 1b #Escape Sequenz 1a 00 70 b2 #1a + Füllbyte + CRC (2Bytes) des gesamten Telegrammes
Das Telegram des eHZ-Zählers ist im Prinzip gleich aufgebaut. Es enthält nur deutlich weniger Informationen. Daher erspare ich mir an dieser Stelle das Listing.
die CRC Prüfung
Zunächst wollte ich testen ob die empfangenen Daten wirklich nicht beschädigt bzw. komplett sind. Im Dokument des BSI sind zur CRC-Prüfung nur recht spärliche Informationen enthalten. Im Prinzip nur, dass es eine CCITT-CRC16 sein soll und Alles außer den letzten beiden Bytes zur Berechnung herangezogen werden muss. Leider gibt es eine Vielzahl von verschiedenen Methoden zur CRC16 Berechnung. So blieb nur das Probieren übrig. Diese Seite hat mir dabei sehr geholfen. Dazu einfach den Datensatz ohne die letzten beiden Bytes einfügen, auf "Hex" umstellen und "Calc CRC-16" klicken. Der Datensatz des MT175-Zählers war dafür allerdings zu lang, weshalb ich einen Datensatz des eHZ-Zählers nutzte. So kommt man zu dem Ergebnis, dass es eine CRC-16/X-25 Prüfung ist. Wichtig sind hier folgende Parameter:- Init=FFFF(HEX) (Anfangswert)
- Poly=1021(HEX) (Polynom)
- RefIn=true (Reverse in)
- RefOut=true (Reverse out)
- XorOut=FFFF(HEX) mit diesem Wert das Ergebnis verXOdern
Vorbereitung des Raspberry Pi
Es kommt ein Raspberry Pi mit einer identischen externen Hardware wie beim Telegram-Bot zum Einsatz. Bis auf das Rolladenscript und dessen Servicebeschreibung ist die Grundkonfiguration also die gleiche.Mit folgender Methode kann man leicht heraus finden, an welchem USB-Port das Interface angeschlossen wurde:
- USB-Kabel des Interfaces vom Rasberry Pi trennen
- kurz warten
- USB-Kabel wieder verbinden
- folgenden Befehl in die Konsole eingeben:
dmesg
Anschließend sieht man eine Auflistung der jüngsten Ereignisse am Betriebssystem.
In einer der letzten Zeilen sollte eine ttyUSBx genannt werden.erste Daten
Ist der Raspberry so vorbereitet, kann schon das erste Testscript laufen. Bei mir ist das SML-Interface für den MT175-Zähler an ttyUSB1 angeschlossen. Das Interface für den eHZ-Zähler an ttyUSB0. Das Test-Script hat folgende Funktionen:- - Öffnen der seriellen Schnittstelle ttyUSB1 mit den vorgegebenen Parametern
- - eventuelle Daten in den Puffern der seriellen Schnittstelle löschen
- - Empfang und Speicherung der vom Zähler gesendeten Daten
- - Erkennung der einzelnen Telegramme über die kurze Pause zwischen zwei Telegrammen (über einen Watchdog)
- - nach Empfang eines kompletten Telegramms:
- - Ausgabe der Rohdaten auf der Konsole
- - CRC Prüfung
- - Prüfung auf SML Startsequenz
Dazu eine Datei erzeugen, ausfürbar machen, und im Editor öffnen:
touch /home/pi/sml/1th_test_usb1.py chmod +x /home/pi/sml/1th_test_usb1.py nano /home/pi/sml/1th_test_usb1.pyFolgenden Inhalt in die Datei schreiben und speichern:
#!/usr/bin/python # -*- coding: utf-8 -*- import time import serial from threading import Timer import sys mystring = "" crc16_x25_table = [ 0x0000, 0x1189, 0x2312, 0x329B, 0x4624, 0x57AD, 0x6536, 0x74BF, 0x8C48, 0x9DC1, 0xAF5A, 0xBED3, 0xCA6C, 0xDBE5, 0xE97E, 0xF8F7, 0x1081, 0x0108, 0x3393, 0x221A, 0x56A5, 0x472C, 0x75B7, 0x643E, 0x9CC9, 0x8D40, 0xBFDB, 0xAE52, 0xDAED, 0xCB64, 0xF9FF, 0xE876, 0x2102, 0x308B, 0x0210, 0x1399, 0x6726, 0x76AF, 0x4434, 0x55BD, 0xAD4A, 0xBCC3, 0x8E58, 0x9FD1, 0xEB6E, 0xFAE7, 0xC87C, 0xD9F5, 0x3183, 0x200A, 0x1291, 0x0318, 0x77A7, 0x662E, 0x54B5, 0x453C, 0xBDCB, 0xAC42, 0x9ED9, 0x8F50, 0xFBEF, 0xEA66, 0xD8FD, 0xC974, 0x4204, 0x538D, 0x6116, 0x709F, 0x0420, 0x15A9, 0x2732, 0x36BB, 0xCE4C, 0xDFC5, 0xED5E, 0xFCD7, 0x8868, 0x99E1, 0xAB7A, 0xBAF3, 0x5285, 0x430C, 0x7197, 0x601E, 0x14A1, 0x0528, 0x37B3, 0x263A, 0xDECD, 0xCF44, 0xFDDF, 0xEC56, 0x98E9, 0x8960, 0xBBFB, 0xAA72, 0x6306, 0x728F, 0x4014, 0x519D, 0x2522, 0x34AB, 0x0630, 0x17B9, 0xEF4E, 0xFEC7, 0xCC5C, 0xDDD5, 0xA96A, 0xB8E3, 0x8A78, 0x9BF1, 0x7387, 0x620E, 0x5095, 0x411C, 0x35A3, 0x242A, 0x16B1, 0x0738, 0xFFCF, 0xEE46, 0xDCDD, 0xCD54, 0xB9EB, 0xA862, 0x9AF9, 0x8B70, 0x8408, 0x9581, 0xA71A, 0xB693, 0xC22C, 0xD3A5, 0xE13E, 0xF0B7, 0x0840, 0x19C9, 0x2B52, 0x3ADB, 0x4E64, 0x5FED, 0x6D76, 0x7CFF, 0x9489, 0x8500, 0xB79B, 0xA612, 0xD2AD, 0xC324, 0xF1BF, 0xE036, 0x18C1, 0x0948, 0x3BD3, 0x2A5A, 0x5EE5, 0x4F6C, 0x7DF7, 0x6C7E, 0xA50A, 0xB483, 0x8618, 0x9791, 0xE32E, 0xF2A7, 0xC03C, 0xD1B5, 0x2942, 0x38CB, 0x0A50, 0x1BD9, 0x6F66, 0x7EEF, 0x4C74, 0x5DFD, 0xB58B, 0xA402, 0x9699, 0x8710, 0xF3AF, 0xE226, 0xD0BD, 0xC134, 0x39C3, 0x284A, 0x1AD1, 0x0B58, 0x7FE7, 0x6E6E, 0x5CF5, 0x4D7C, 0xC60C, 0xD785, 0xE51E, 0xF497, 0x8028, 0x91A1, 0xA33A, 0xB2B3, 0x4A44, 0x5BCD, 0x6956, 0x78DF, 0x0C60, 0x1DE9, 0x2F72, 0x3EFB, 0xD68D, 0xC704, 0xF59F, 0xE416, 0x90A9, 0x8120, 0xB3BB, 0xA232, 0x5AC5, 0x4B4C, 0x79D7, 0x685E, 0x1CE1, 0x0D68, 0x3FF3, 0x2E7A, 0xE70E, 0xF687, 0xC41C, 0xD595, 0xA12A, 0xB0A3, 0x8238, 0x93B1, 0x6B46, 0x7ACF, 0x4854, 0x59DD, 0x2D62, 0x3CEB, 0x0E70, 0x1FF9, 0xF78F, 0xE606, 0xD49D, 0xC514, 0xB1AB, 0xA022, 0x92B9, 0x8330, 0x7BC7, 0x6A4E, 0x58D5, 0x495C, 0x3DE3, 0x2C6A, 0x1EF1, 0x0F78] class Watchdog_timer: def __init__(self, timeout, userHandler=None): self.timeout = timeout self.handler = userHandler if userHandler is not None else self.defaultHandler self.timer = Timer(self.timeout, self.handler) self.timer.start() def reset(self): self.timer.cancel() self.timer = Timer(self.timeout, self.handler) self.timer.start() def stop(self): self.timer.cancel() def defaultHandler(self): raise self def crc16_x25(Buffer): crcsum = 0xffff global crc16_x25_table for byte in Buffer: crcsum = crc16_x25_table[(ord(byte) ^ crcsum) & 0xff] ^ (crcsum >> 8 & 0xff) crcsum ^= 0xffff return crcsum def watchdogtimer_ovf(): global mystring sys.stdout.write("SML-Stream:\n" + mystring.encode('hex') + "\n\n") #komplettes Telegram ausgeben message = mystring[0:-2] #letzen beiden Bytes wegschneiden crc_rx = int((mystring[-1] + mystring[-2]).encode('hex'), 16) #CRC Bytes getauscht in eine Variable speichern crc_calc = crc16_x25(message) #eigene Prüfsumme bilden if crc_rx == crc_calc: #Prüfsummen vergleichen: wenn identisch sys.stdout.write("crc OK\n") if message[0:8] == '\x1b\x1b\x1b\x1b\x01\x01\x01\x01': #die ersten 8 Bytes prüfen sys.stdout.write("SML Start erkannt\n\n") watchdog.stop() mystring="" else: sys.stdout.write("kein SML\n\n") mystring="" watchdog.stop() else: sys.stdout.write("crc NOK\n\n") mystring="" watchdog.stop() try: my_tty = serial.Serial(port='/dev/ttyUSB1', baudrate = 9600, parity =serial.PARITY_NONE, stopbits=serial.STOPBITS_ONE, bytesize=serial.EIGHTBITS, timeout=0) sys.stdout.write(my_tty.portstr + " geöffnet\n\n") my_tty.close() my_tty.open() except Exception, e: sys.stdout.write("serieller Port konnte nicht geöffnet werden:\n" + str(e) + "\n\n") exit() try: my_tty.reset_input_buffer() my_tty.reset_output_buffer() watchdog = Watchdog_timer(0.1, watchdogtimer_ovf) watchdog.stop() while True: while my_tty.in_waiting > 0: mystring += my_tty.read() watchdog.reset() except KeyboardInterrupt: my_tty.close() sys.stdout.write("\nProgramm wurde manuell beendet!\n")
Die eben erzeugte Datei ausführen:
/home/pi/sml/1th_test_usb1.pyNun sollten die ankommenden Telegramme auf der Konsole zu sehen sein.
Mit der Tastenkombination [STRG]+[C] kann man das Programm stoppen.
lesbar
Das folgende Programm schlüsselt alle im Telegramm enthaltenen Informationen auf. Später wird die Ausgabe auf die für mich wichtigen Informationen reduziert. Da sich gleiche Informationen immer an der selben Stelle befinden, habe ich hier auf einen aufwändigen "Parser" verzichtet. Die Ausgabe erfolgt wieder auf der Konsole#!/usr/bin/python # -*- coding: utf-8 -*- import time import serial from threading import Timer import sys mystring = "" crc16_x25_table = [ 0x0000, 0x1189, 0x2312, 0x329B, 0x4624, 0x57AD, 0x6536, 0x74BF, 0x8C48, 0x9DC1, 0xAF5A, 0xBED3, 0xCA6C, 0xDBE5, 0xE97E, 0xF8F7, 0x1081, 0x0108, 0x3393, 0x221A, 0x56A5, 0x472C, 0x75B7, 0x643E, 0x9CC9, 0x8D40, 0xBFDB, 0xAE52, 0xDAED, 0xCB64, 0xF9FF, 0xE876, 0x2102, 0x308B, 0x0210, 0x1399, 0x6726, 0x76AF, 0x4434, 0x55BD, 0xAD4A, 0xBCC3, 0x8E58, 0x9FD1, 0xEB6E, 0xFAE7, 0xC87C, 0xD9F5, 0x3183, 0x200A, 0x1291, 0x0318, 0x77A7, 0x662E, 0x54B5, 0x453C, 0xBDCB, 0xAC42, 0x9ED9, 0x8F50, 0xFBEF, 0xEA66, 0xD8FD, 0xC974, 0x4204, 0x538D, 0x6116, 0x709F, 0x0420, 0x15A9, 0x2732, 0x36BB, 0xCE4C, 0xDFC5, 0xED5E, 0xFCD7, 0x8868, 0x99E1, 0xAB7A, 0xBAF3, 0x5285, 0x430C, 0x7197, 0x601E, 0x14A1, 0x0528, 0x37B3, 0x263A, 0xDECD, 0xCF44, 0xFDDF, 0xEC56, 0x98E9, 0x8960, 0xBBFB, 0xAA72, 0x6306, 0x728F, 0x4014, 0x519D, 0x2522, 0x34AB, 0x0630, 0x17B9, 0xEF4E, 0xFEC7, 0xCC5C, 0xDDD5, 0xA96A, 0xB8E3, 0x8A78, 0x9BF1, 0x7387, 0x620E, 0x5095, 0x411C, 0x35A3, 0x242A, 0x16B1, 0x0738, 0xFFCF, 0xEE46, 0xDCDD, 0xCD54, 0xB9EB, 0xA862, 0x9AF9, 0x8B70, 0x8408, 0x9581, 0xA71A, 0xB693, 0xC22C, 0xD3A5, 0xE13E, 0xF0B7, 0x0840, 0x19C9, 0x2B52, 0x3ADB, 0x4E64, 0x5FED, 0x6D76, 0x7CFF, 0x9489, 0x8500, 0xB79B, 0xA612, 0xD2AD, 0xC324, 0xF1BF, 0xE036, 0x18C1, 0x0948, 0x3BD3, 0x2A5A, 0x5EE5, 0x4F6C, 0x7DF7, 0x6C7E, 0xA50A, 0xB483, 0x8618, 0x9791, 0xE32E, 0xF2A7, 0xC03C, 0xD1B5, 0x2942, 0x38CB, 0x0A50, 0x1BD9, 0x6F66, 0x7EEF, 0x4C74, 0x5DFD, 0xB58B, 0xA402, 0x9699, 0x8710, 0xF3AF, 0xE226, 0xD0BD, 0xC134, 0x39C3, 0x284A, 0x1AD1, 0x0B58, 0x7FE7, 0x6E6E, 0x5CF5, 0x4D7C, 0xC60C, 0xD785, 0xE51E, 0xF497, 0x8028, 0x91A1, 0xA33A, 0xB2B3, 0x4A44, 0x5BCD, 0x6956, 0x78DF, 0x0C60, 0x1DE9, 0x2F72, 0x3EFB, 0xD68D, 0xC704, 0xF59F, 0xE416, 0x90A9, 0x8120, 0xB3BB, 0xA232, 0x5AC5, 0x4B4C, 0x79D7, 0x685E, 0x1CE1, 0x0D68, 0x3FF3, 0x2E7A, 0xE70E, 0xF687, 0xC41C, 0xD595, 0xA12A, 0xB0A3, 0x8238, 0x93B1, 0x6B46, 0x7ACF, 0x4854, 0x59DD, 0x2D62, 0x3CEB, 0x0E70, 0x1FF9, 0xF78F, 0xE606, 0xD49D, 0xC514, 0xB1AB, 0xA022, 0x92B9, 0x8330, 0x7BC7, 0x6A4E, 0x58D5, 0x495C, 0x3DE3, 0x2C6A, 0x1EF1, 0x0F78] class Watchdog_timer: def __init__(self, timeout, userHandler=None): self.timeout = timeout self.handler = userHandler if userHandler is not None else self.defaultHandler self.timer = Timer(self.timeout, self.handler) self.timer.start() def reset(self): self.timer.cancel() self.timer = Timer(self.timeout, self.handler) self.timer.start() def stop(self): self.timer.cancel() def defaultHandler(self): raise self def crc16_x25(Buffer): crcsum = 0xffff global crc16_x25_table for byte in Buffer: crcsum = crc16_x25_table[(ord(byte) ^ crcsum) & 0xff] ^ (crcsum >> 8 & 0xff) crcsum ^= 0xffff return crcsum def watchdogtimer_ovf(): global mystring sys.stdout.write("SML-Stream:\n" + mystring.encode('hex') + "\n\n") #komplettes Telegram ausgeben message = mystring[0:-2] #letzen beiden Bytes wegschneiden crc_rx = int((mystring[-1] + mystring[-2]).encode('hex'), 16) #CRC Bytes getauscht in eine Variable speichern crc_calc = crc16_x25(message) #eigene Prüfsumme bilden if crc_rx == crc_calc: #Prüfsummen vergleichen: wenn identisch sys.stdout.write("crc OK\n") if message[0:8] == '\x1b\x1b\x1b\x1b\x01\x01\x01\x01': #die ersten 8 Bytes prüfen sys.stdout.write("SML Start erkannt\n\n") sys.stdout.write("____________________________________\n") sys.stdout.write("TransactionId: " + message[10:14].encode('hex') + "\n") sys.stdout.write("GroupNo: " + message[15:16].encode('hex') + "\n") sys.stdout.write("abortOnError: " + message[17:18].encode('hex') + "\n") sys.stdout.write("getOpenResponse: " + message[20:22].encode('hex') + "\n") sys.stdout.write("reqFileId: " + message[26:30].encode('hex') + "\n") sys.stdout.write("serverId: " + message[31:41].encode('hex') + "\n") sys.stdout.write("crc: " + message[44:46].encode('hex') + "\n") sys.stdout.write("____________________________________\n") sys.stdout.write("TransactionId: " + message[49:53].encode('hex') + "\n") sys.stdout.write("GroupNo: " + message[54:55].encode('hex') + "\n") sys.stdout.write("abortOnError: " + message[56:57].encode('hex') + "\n") sys.stdout.write("getListResponse: " + message[59:61].encode('hex') + "\n") sys.stdout.write("serverId: " + message[64:74].encode('hex') + "\n") sys.stdout.write("listName: " + message[75:81].encode('hex') + "\n") sys.stdout.write("___\n") sys.stdout.write("choice(01=secIndex): " + message[83:84].encode('hex') + "\n") sys.stdout.write("secIndex(uptime): " + str(int(message[85:89].encode('hex'),16)) + "\n") sys.stdout.write("___\n") sys.stdout.write("objName: " + message[92:98].encode('hex') + " = OBIS-Kennzahl für Herstellerkennung\n") sys.stdout.write("Herstellerkennung: " + message[103:106] + "\n") sys.stdout.write("___\n") sys.stdout.write("objName: " + message[109:115].encode('hex') + " = OBIS-Kennzahl für ServerID\n") sys.stdout.write("ServerID: " + message[120:130].encode('hex') + "\n") sys.stdout.write("___\n") sys.stdout.write("objName: " + message[133:139].encode('hex') + " = OBIS-Kennzahl für Wirkenergie Bezug gesamt tariflos\n") sys.stdout.write("???: " + message[140:144].encode('hex') + "\n") sys.stdout.write("unit: " + message[146:147].encode('hex') + " (Einheit 1E=Wh)\n") sys.stdout.write("scaler: " + message[148:149].encode('hex') + " (Multiplikator int(ff)=-1 10^-1 =0.1)\n") sys.stdout.write("Bezug: " + str(int(message[150:158].encode('hex'),16)*0.1) + " Wh\n") sys.stdout.write("___\n") sys.stdout.write("objName: " + message[209:215].encode('hex') + " OBIS-Kennzahl für Wirkenergie Einspeisung gesamt tariflos\n") sys.stdout.write("unit: " + message[218:219].encode('hex') + " (Einheit 1E=Wh)\n") sys.stdout.write("scaler: " + message[220:221].encode('hex') + " (Multiplikator int(ff)=-1 10^-1 =0.1)\n") sys.stdout.write("Einspeisung: " + str(int(message[222:229].encode('hex'),16)*0.1) + " Wh\n") sys.stdout.write("___\n") sys.stdout.write("objName: " + message[281:287].encode('hex') + " OBIS-Kennzahl für Momentanwert Wirkleistung gesamt\n") sys.stdout.write("unit: " + message[290:291].encode('hex') + " (Einheit 1B=W)\n") sys.stdout.write("scaler: " + message[292:293].encode('hex')+ " Multiplikator 10^0 = 1\n") sys.stdout.write("Momentanwert Wirkleistung gesamt: " + str(int(message[294:298].encode('hex'),16))+ " W\n") sys.stdout.write("___\n") sys.stdout.write("objName: " + message[301:307].encode('hex') + " OBIS-Kennzahl für Momentanwert Wirkleistung L1\n") sys.stdout.write("unit: " + message[310:311].encode('hex') + " (Einheit 1B=W)\n") sys.stdout.write("scaler: " + message[312:313].encode('hex')+ " Multiplikator 10^0 = 1\n") sys.stdout.write("Momentanwert Wirkleistung L1: " + str(int(message[314:318].encode('hex'),16))+ " W\n") sys.stdout.write("___\n") sys.stdout.write("objName: " + message[321:327].encode('hex') + " OBIS-Kennzahl für Momentanwert Wirkleistung L2\n") sys.stdout.write("unit: " + message[330:331].encode('hex') + " (Einheit 1B=W)\n") sys.stdout.write("scaler: " + message[332:333].encode('hex')+ " Multiplikator 10^0 = 1\n") sys.stdout.write("Momentanwert Wirkleistung L2: " + str(int(message[334:338].encode('hex'),16))+ " W\n") sys.stdout.write("___\n") sys.stdout.write("objName: " + message[341:347].encode('hex') + " OBIS-Kennzahl für Momentanwert Wirkleistung L3\n") sys.stdout.write("unit: " + message[350:351].encode('hex') + " (Einheit 1B=W)\n") sys.stdout.write("scaler: " + message[352:353].encode('hex')+ " Multiplikator 10^0 = 1\n") sys.stdout.write("Momentanwert Wirkleistung L3: " + str(int(message[354:358].encode('hex'),16))+ " W\n") sys.stdout.write("___\n") sys.stdout.write("objName: " + message[361:367].encode('hex') + " OBIS-Kennzahl für Public Key\n") sys.stdout.write("value: " + message[372:421].encode('hex') + " (Public Key)\n") sys.stdout.write("___\n") sys.stdout.write("crc: " + message[425:427].encode('hex') + "\n") sys.stdout.write("____________________________________\n") sys.stdout.write("TransactionId: " + message[430:434].encode('hex') + "\n") sys.stdout.write("GroupNo: " + message[435:436].encode('hex') + "\n") sys.stdout.write("abortOnError: " + message[437:438].encode('hex') + "\n") sys.stdout.write("getCloseResponse: " + message[440:442].encode('hex') + "\n") sys.stdout.write("crc: " + message[445:447].encode('hex') + "\n") sys.stdout.write("\n") watchdog.stop() mystring="" else: sys.stdout.write("kein SML\n\n") mystring="" watchdog.stop() else: sys.stdout.write("crc NOK\n\n") mystring="" watchdog.stop() try: my_tty = serial.Serial(port='/dev/ttyUSB1', baudrate = 9600, parity =serial.PARITY_NONE, stopbits=serial.STOPBITS_ONE, bytesize=serial.EIGHTBITS, timeout=0) sys.stdout.write(my_tty.portstr + " geöffnet\n\n") my_tty.close() my_tty.open() except Exception, e: sys.stdout.write("serieller Port konnte nicht geöffnet werden:\n" + str(e) + "\n\n") exit() try: my_tty.reset_input_buffer() my_tty.reset_output_buffer() watchdog = Watchdog_timer(0.1, watchdogtimer_ovf) watchdog.stop() while True: while my_tty.in_waiting > 0: mystring += my_tty.read() watchdog.reset() except KeyboardInterrupt: my_tty.close() sys.stdout.write("\nProgramm wurde manuell beendet!\n")
graphisch aufbereitet
Die Messwerte sollen später auf einer Webseite graphisch aufbereitet angezeigt werden. Dazu speichere ich die relevanten Daten im so genannten JSON-Format in eine Datei. Um die SD-Karte zu schonen(Stichwort Schreibzyklen), liegt diese Datei in einer RAMDisk-Partition - also einer Partition, die sich im Arbeitsspeicher des Raspberry Pi befindet. Zur Bereitstellung der RAMDisk sind folgende Schritte nötig:Mountpoint anlegen:
sudo mkdir /mnt/RAMDisk
Die Datei "/etc/fstab" mit einem Editor öffnen z.B.:
sudo nano /etc/fstabIn der letzten Zeile den Text "tmpfs /mnt/RAMDisk tmpfs nodev,nosuid,size=256k 0 0"ergänzen.
Mit [STRG]+[X] den Editor beenden und die Änderungen mit [j] [ENTER]speichern.
Nun ist die Filesystem-Tabelle aktuell - es werden 256kByte des Arbeitsspeichers als RAMDisk reserviert.
alle Filesysteme neu mounten:
sudo mount -a
Das folgende Script ist die finale Version, welche bei mir auf dem Raspberry Pi läuft. (für den MT175-Zähler)
#!/usr/bin/python # -*- coding: utf-8 -*- import time import serial import sys import json from threading import Timer mystring = "" crc16_x25_table = [ 0x0000, 0x1189, 0x2312, 0x329B, 0x4624, 0x57AD, 0x6536, 0x74BF, 0x8C48, 0x9DC1, 0xAF5A, 0xBED3, 0xCA6C, 0xDBE5, 0xE97E, 0xF8F7, 0x1081, 0x0108, 0x3393, 0x221A, 0x56A5, 0x472C, 0x75B7, 0x643E, 0x9CC9, 0x8D40, 0xBFDB, 0xAE52, 0xDAED, 0xCB64, 0xF9FF, 0xE876, 0x2102, 0x308B, 0x0210, 0x1399, 0x6726, 0x76AF, 0x4434, 0x55BD, 0xAD4A, 0xBCC3, 0x8E58, 0x9FD1, 0xEB6E, 0xFAE7, 0xC87C, 0xD9F5, 0x3183, 0x200A, 0x1291, 0x0318, 0x77A7, 0x662E, 0x54B5, 0x453C, 0xBDCB, 0xAC42, 0x9ED9, 0x8F50, 0xFBEF, 0xEA66, 0xD8FD, 0xC974, 0x4204, 0x538D, 0x6116, 0x709F, 0x0420, 0x15A9, 0x2732, 0x36BB, 0xCE4C, 0xDFC5, 0xED5E, 0xFCD7, 0x8868, 0x99E1, 0xAB7A, 0xBAF3, 0x5285, 0x430C, 0x7197, 0x601E, 0x14A1, 0x0528, 0x37B3, 0x263A, 0xDECD, 0xCF44, 0xFDDF, 0xEC56, 0x98E9, 0x8960, 0xBBFB, 0xAA72, 0x6306, 0x728F, 0x4014, 0x519D, 0x2522, 0x34AB, 0x0630, 0x17B9, 0xEF4E, 0xFEC7, 0xCC5C, 0xDDD5, 0xA96A, 0xB8E3, 0x8A78, 0x9BF1, 0x7387, 0x620E, 0x5095, 0x411C, 0x35A3, 0x242A, 0x16B1, 0x0738, 0xFFCF, 0xEE46, 0xDCDD, 0xCD54, 0xB9EB, 0xA862, 0x9AF9, 0x8B70, 0x8408, 0x9581, 0xA71A, 0xB693, 0xC22C, 0xD3A5, 0xE13E, 0xF0B7, 0x0840, 0x19C9, 0x2B52, 0x3ADB, 0x4E64, 0x5FED, 0x6D76, 0x7CFF, 0x9489, 0x8500, 0xB79B, 0xA612, 0xD2AD, 0xC324, 0xF1BF, 0xE036, 0x18C1, 0x0948, 0x3BD3, 0x2A5A, 0x5EE5, 0x4F6C, 0x7DF7, 0x6C7E, 0xA50A, 0xB483, 0x8618, 0x9791, 0xE32E, 0xF2A7, 0xC03C, 0xD1B5, 0x2942, 0x38CB, 0x0A50, 0x1BD9, 0x6F66, 0x7EEF, 0x4C74, 0x5DFD, 0xB58B, 0xA402, 0x9699, 0x8710, 0xF3AF, 0xE226, 0xD0BD, 0xC134, 0x39C3, 0x284A, 0x1AD1, 0x0B58, 0x7FE7, 0x6E6E, 0x5CF5, 0x4D7C, 0xC60C, 0xD785, 0xE51E, 0xF497, 0x8028, 0x91A1, 0xA33A, 0xB2B3, 0x4A44, 0x5BCD, 0x6956, 0x78DF, 0x0C60, 0x1DE9, 0x2F72, 0x3EFB, 0xD68D, 0xC704, 0xF59F, 0xE416, 0x90A9, 0x8120, 0xB3BB, 0xA232, 0x5AC5, 0x4B4C, 0x79D7, 0x685E, 0x1CE1, 0x0D68, 0x3FF3, 0x2E7A, 0xE70E, 0xF687, 0xC41C, 0xD595, 0xA12A, 0xB0A3, 0x8238, 0x93B1, 0x6B46, 0x7ACF, 0x4854, 0x59DD, 0x2D62, 0x3CEB, 0x0E70, 0x1FF9, 0xF78F, 0xE606, 0xD49D, 0xC514, 0xB1AB, 0xA022, 0x92B9, 0x8330, 0x7BC7, 0x6A4E, 0x58D5, 0x495C, 0x3DE3, 0x2C6A, 0x1EF1, 0x0F78] class Watchdog_timer: def __init__(self, timeout, userHandler=None): self.timeout = timeout self.handler = userHandler if userHandler is not None else self.defaultHandler self.timer = Timer(self.timeout, self.handler) self.timer.start() def reset(self): self.timer.cancel() self.timer = Timer(self.timeout, self.handler) self.timer.start() def stop(self): self.timer.cancel() def defaultHandler(self): raise self def crc16_x25(Buffer): crcsum = 0xffff global crc16_x25_table for byte in Buffer: crcsum = crc16_x25_table[(ord(byte) ^ crcsum) & 0xff] ^ (crcsum >> 8 & 0xff) crcsum ^= 0xffff return crcsum def watchdogtimer_ovf(): global mystring message = mystring[0:-2] crc_rx = int((mystring[-1] + mystring[-2]).encode('hex'), 16) crc_calc = crc16_x25(message) if crc_rx == crc_calc: if message[0:8] == '\x1b\x1b\x1b\x1b\x01\x01\x01\x01': watchdog.stop() AB_Wges = round((int(message[150:158].encode('hex'),16)*0.0001),4) AB_Pges = (int(message[294:298].encode('hex'),16)) AB_PL1 = (int(message[314:318].encode('hex'),16)) AB_PL2 = (int(message[334:338].encode('hex'),16)) AB_PL3 = (int(message[354:358].encode('hex'),16)) mystring="" result={"AB_Pges": AB_Pges,"AB_PL1": AB_PL1,"AB_PL2": AB_PL2,"AB_PL3": AB_PL3,"AB_Wges": AB_Wges} with open('/mnt/RAMDisk/AB.json', 'w') as file: json.dump(result, file) else: mystring="" watchdog.stop() else: mystring="" watchdog.stop() try: my_tty = serial.Serial(port='/dev/ttyUSB1', baudrate = 9600, parity =serial.PARITY_NONE, stopbits=serial.STOPBITS_ONE, bytesize=serial.EIGHTBITS, timeout=0) my_tty.close() my_tty.open() except Exception, e: exit() try: my_tty.reset_input_buffer() my_tty.reset_output_buffer() watchdog = Watchdog_timer(0.1, watchdogtimer_ovf) watchdog.stop() while True: while my_tty.in_waiting > 0: mystring += my_tty.read() watchdog.reset() except KeyboardInterrupt: my_tty.close() sys.stdout.write("\nProgramm wurde manuell beendet!\n")
Der Vollständigkeit halber noch das finale Script für den eHZ-Zähler:
#!/usr/bin/python # -*- coding: utf-8 -*- import time import serial import sys import json from threading import Timer mystring = "" crc16_x25_table = [ 0x0000, 0x1189, 0x2312, 0x329B, 0x4624, 0x57AD, 0x6536, 0x74BF, 0x8C48, 0x9DC1, 0xAF5A, 0xBED3, 0xCA6C, 0xDBE5, 0xE97E, 0xF8F7, 0x1081, 0x0108, 0x3393, 0x221A, 0x56A5, 0x472C, 0x75B7, 0x643E, 0x9CC9, 0x8D40, 0xBFDB, 0xAE52, 0xDAED, 0xCB64, 0xF9FF, 0xE876, 0x2102, 0x308B, 0x0210, 0x1399, 0x6726, 0x76AF, 0x4434, 0x55BD, 0xAD4A, 0xBCC3, 0x8E58, 0x9FD1, 0xEB6E, 0xFAE7, 0xC87C, 0xD9F5, 0x3183, 0x200A, 0x1291, 0x0318, 0x77A7, 0x662E, 0x54B5, 0x453C, 0xBDCB, 0xAC42, 0x9ED9, 0x8F50, 0xFBEF, 0xEA66, 0xD8FD, 0xC974, 0x4204, 0x538D, 0x6116, 0x709F, 0x0420, 0x15A9, 0x2732, 0x36BB, 0xCE4C, 0xDFC5, 0xED5E, 0xFCD7, 0x8868, 0x99E1, 0xAB7A, 0xBAF3, 0x5285, 0x430C, 0x7197, 0x601E, 0x14A1, 0x0528, 0x37B3, 0x263A, 0xDECD, 0xCF44, 0xFDDF, 0xEC56, 0x98E9, 0x8960, 0xBBFB, 0xAA72, 0x6306, 0x728F, 0x4014, 0x519D, 0x2522, 0x34AB, 0x0630, 0x17B9, 0xEF4E, 0xFEC7, 0xCC5C, 0xDDD5, 0xA96A, 0xB8E3, 0x8A78, 0x9BF1, 0x7387, 0x620E, 0x5095, 0x411C, 0x35A3, 0x242A, 0x16B1, 0x0738, 0xFFCF, 0xEE46, 0xDCDD, 0xCD54, 0xB9EB, 0xA862, 0x9AF9, 0x8B70, 0x8408, 0x9581, 0xA71A, 0xB693, 0xC22C, 0xD3A5, 0xE13E, 0xF0B7, 0x0840, 0x19C9, 0x2B52, 0x3ADB, 0x4E64, 0x5FED, 0x6D76, 0x7CFF, 0x9489, 0x8500, 0xB79B, 0xA612, 0xD2AD, 0xC324, 0xF1BF, 0xE036, 0x18C1, 0x0948, 0x3BD3, 0x2A5A, 0x5EE5, 0x4F6C, 0x7DF7, 0x6C7E, 0xA50A, 0xB483, 0x8618, 0x9791, 0xE32E, 0xF2A7, 0xC03C, 0xD1B5, 0x2942, 0x38CB, 0x0A50, 0x1BD9, 0x6F66, 0x7EEF, 0x4C74, 0x5DFD, 0xB58B, 0xA402, 0x9699, 0x8710, 0xF3AF, 0xE226, 0xD0BD, 0xC134, 0x39C3, 0x284A, 0x1AD1, 0x0B58, 0x7FE7, 0x6E6E, 0x5CF5, 0x4D7C, 0xC60C, 0xD785, 0xE51E, 0xF497, 0x8028, 0x91A1, 0xA33A, 0xB2B3, 0x4A44, 0x5BCD, 0x6956, 0x78DF, 0x0C60, 0x1DE9, 0x2F72, 0x3EFB, 0xD68D, 0xC704, 0xF59F, 0xE416, 0x90A9, 0x8120, 0xB3BB, 0xA232, 0x5AC5, 0x4B4C, 0x79D7, 0x685E, 0x1CE1, 0x0D68, 0x3FF3, 0x2E7A, 0xE70E, 0xF687, 0xC41C, 0xD595, 0xA12A, 0xB0A3, 0x8238, 0x93B1, 0x6B46, 0x7ACF, 0x4854, 0x59DD, 0x2D62, 0x3CEB, 0x0E70, 0x1FF9, 0xF78F, 0xE606, 0xD49D, 0xC514, 0xB1AB, 0xA022, 0x92B9, 0x8330, 0x7BC7, 0x6A4E, 0x58D5, 0x495C, 0x3DE3, 0x2C6A, 0x1EF1, 0x0F78] class Watchdog_timer: def __init__(self, timeout, userHandler=None): self.timeout = timeout self.handler = userHandler if userHandler is not None else self.defaultHandler self.timer = Timer(self.timeout, self.handler) self.timer.start() def reset(self): self.timer.cancel() self.timer = Timer(self.timeout, self.handler) self.timer.start() def stop(self): self.timer.cancel() def defaultHandler(self): raise self def crc16_x25(Buffer): crcsum = 0xffff global crc16_x25_table for byte in Buffer: crcsum = crc16_x25_table[(ord(byte) ^ crcsum) & 0xff] ^ (crcsum >> 8 & 0xff) crcsum ^= 0xffff return crcsum def watchdogtimer_ovf(): global mystring message = mystring[0:-2] crc_rx = int((mystring[-1] + mystring[-2]).encode('hex'), 16) crc_calc = crc16_x25(message) if crc_rx == crc_calc: if message[0:8] == '\x1b\x1b\x1b\x1b\x01\x01\x01\x01': watchdog.stop() WP_Wges = round((int(message[141:146].encode('hex'),16)*0.0001),4) WP_Pges = round((int(message[186:190].encode('hex'),16)*0.1),1) mystring="" result={"WP_Pges": WP_Pges,"WP_Wges": WP_Wges} with open('/mnt/RAMDisk/WP.json', 'w') as file: json.dump(result, file) else: mystring="" watchdog.stop() else: mystring="" watchdog.stop() try: my_tty = serial.Serial(port='/dev/ttyUSB0', baudrate = 9600, parity =serial.PARITY_NONE, stopbits=serial.STOPBITS_ONE, bytesize=serial.EIGHTBITS, timeout=0) my_tty.close() my_tty.open() except Exception, e: exit() try: my_tty.reset_input_buffer() my_tty.reset_output_buffer() watchdog = Watchdog_timer(0.1, watchdogtimer_ovf) watchdog.stop() while True: while my_tty.in_waiting > 0: mystring += my_tty.read() watchdog.reset() except KeyboardInterrupt: my_tty.close() sys.stdout.write("\nProgramm wurde manuell beendet!\n")
Diese beiden Scripte müssen jetzt noch als Dienste beschrieben und gestartet werden. Wie das anzustellen ist, kann man hier ableiten. Deßhalb an dieser Stelle nur die Kurzform:
sudo nano /etc/systemd/system/AB_sml.daemon.service
[Unit] Description=SML Parser fuer allgemeinen Bedarf After=multi-user.target [Service] Type=idle ExecStart=/usr/bin/python /home/pi/sml/sml_parser_AB_usb1.py Restart=on-failure RestartSec=1m [Install] WantedBy=multi-user.target
sudo nano /etc/systemd/system/WP_sml.daemon.service
[Unit] Description=SML Parser fuer Waermepumpe After=multi-user.target [Service] Type=idle ExecStart=/usr/bin/python /home/pi/sml/sml_parser_WP_usb0.py Restart=on-failure RestartSec=1m [Install] WantedBy=multi-user.target
sudo systemctl daemon-reload sudo systemctl enable AB_sml.daemon.service sudo systemctl enable WP_sml.daemon.service sudo reboot
die Webseite
Damit der Raspberry Pi als Webserver arbeiten kann, sind wieder einige Schritte notwendig:Apache Server und PHP installieren:
sudo apt-get install apache2 sudo apt-get install php sudo reboot
Die Daten auf der Webseite sollen über ein JavaScript etwa alle 2 Sekunden aktualisiert werden, ohne dabei allerdings die komplette Webseite neu laden zu müssen. Für mich war die einfachste Lösung: Ein CGI-Script, welches die JSON-Daten auf Anfrage der Webseite übermittelt. Damit CGI-Scripte funktionieren sind wieder ein paar Einstellungen nötig.
Das entsprechende Apache-Modul aktivieren:
sudo a2enmod cgid sudo service apache2 restart
Standardmäßig sind Python-Scripte nicht als CGI-Scripte zugelassen.
Aus diesem Grund muss man es wie folgt "erlauben".
Diese Datei öffnen...
sudo nano /etc/apache2/conf-enabled/serve-cgi-bin.conf
...und an gezeigter Stelle die Zeile "AddHandler cgi-script .py" einfügen:
<Directory "/usr/lib/cgi-bin"> ... ... ... AddHandler cgi-script .py #nur diese Zeile einfügen </Directory>
Damit die Änderungen wirksam werden, Apache neu starten.
sudo service apache2 restart
Nun müssen noch die beiden CGI-Scripte geschrieben werden. Wieder in Kurzform:
sudo touch /usr/lib/cgi-bin/get_AB_json.py sudo chmod +x /usr/lib/cgi-bin/get_AB_json.py sudo nano /usr/lib/cgi-bin/get_AB_json.py
#!/usr/bin/python print "Content-type:text\n" import cgi import json file = open("/mnt/RAMDisk/AB.json", "r") lines = file.readlines() file.close() print lines[0]
sudo touch /usr/lib/cgi-bin/get_AB_json.py sudo chmod +x /usr/lib/cgi-bin/get_WP_json.py sudo nano /usr/lib/cgi-bin/get_WP_json.py
#!/usr/bin/python print "Content-type:text\n" import cgi import json file = open("/mnt/RAMDisk/WP.json", "r") lines = file.readlines() file.close() print lines[0]
Die Dateien für den "Webauftritt" müssen sich in folgendem Pfad befinden:
/var/www/html
Hier ist der Quellcode für die HTML-Seite. Diese ist für eine Auflösung von 800x480 Pixeln optimiert (für das offizielle Raspberry Pi 7" Touchdisplay).
Zu erklären, wie das für die private Nutzung kostenfreie Highcharts© funktioniert, würde an dieser Stelle zu weit führen. Es soll nur deutlich gemacht werden, wie die JSON-Datei abgeholt und die Webseite partiell aktualisiert wird.
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN""http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <meta name="Author" content="Stefan Weigert"> <meta name="DESCRIPTION" content="SML Testseite"> <meta name="PAGE-CONTENT" content="Elektronik"> <meta name="lang" content="de"> <meta name="ROBOTS" content="INDEX,FOLLOW"> <meta name="REVISIT-AFTER" content="60 days"> <meta name="KeyWords" lang="de" content="SML, Smartmeter, FTDI"> <title>SML Testseite</title> <link href="css/style2.css" rel="stylesheet" type="text/css"> <script type="text/javascript" src="js/highcharts.js"></script> <script type="text/javascript" src="js/highcharts-more.js"></script> <script type="text/javascript" src="js/solid-gauge.js"></script> <script type="text/javascript" src="js/jquery-3.3.1.min.js"></script> </head> <body> <div id="text_body"> <div style="width: 790px; height: 470px; margin: 0 auto; background-color: #CCCCCC"> <div style="width: 489px; background-color: #FFFFFF; float: left"> <div style="height: 40px"> <center><h1>allgemeiner Bedarf</h1></center> </div> <div id="container-AB_Pges" style="height: 200px"></div> <div style="height: 175px"> <div id="container-AB_PL1" style="width: 163px; height: 175px; float: left"></div> <div id="container-AB_PL2" style="width: 163px; height: 175px; float: left"></div> <div id="container-AB_PL3" style="width: 163px; height: 175px; float: left"></div> </div> <div id="container-AB_Wges" style="height: 30px; padding: 10px"> <script type="text/javascript"> AB_Wges = ((0).toFixed(4)); var str_AB_Wges = ('<center><div class="segfontbk">' + AB_Wges.split(".")[0] + '<\/div><div class="komma">,<\/div><div class="segfontbk">' + AB_Wges.split(".")[1] + '<\/div>kWh<\/center>'); document.write(str_AB_Wges); </script> </div> </div> <div style="width: 296px; background-color: #FFFFFF; float: right"> <div style="height: 40px"> <center><h1>Wärmepumpe</h1></center> </div> <div id="container-WP_Pges" style="height: 200px"></div> <div style="height: 175px;"></div> <div id="container-WP_Wges" style="height: 30px; padding: 10px"> <script type="text/javascript"> WP_Wges = ((0).toFixed(4)); var str_WP_Wges = ('<center><div class="segfontbk">' + WP_Wges.split(".")[0] + '<\/div><div class="komma">,<\/div><div class="segfontbk">' + WP_Wges.split(".")[1] + '<\/div>kWh<\/center>'); document.write(str_WP_Wges); </script> </div> </div> </div> <script type="text/javascript"> // globale Einstellungen der Gauges var gaugeOptions = { chart: { type: 'solidgauge', style: { fontFamily: 'Dosis, sans-serif' } }, title: null, pane: { center: ['50%', '85%'], size: '100%', startAngle: -90, endAngle: 90, background: { backgroundColor: (Highcharts.theme && Highcharts.theme.background2) || '#EEE', innerRadius: '60%', outerRadius: '100%', shape: 'arc' } }, credits: { enabled: false }, tooltip: { enabled: false }, yAxis: { stops: [ [0.1, '#55BF3B'], [0.5, '#DDDF0D'], [0.9, '#DF5353'] ], lineWidth: 0, minorTickInterval: null, tickAmount: 2, labels: { y: 16 } }, plotOptions: { solidgauge: { dataLabels: { y: 15, borderWidth: 0, useHTML: true } } } }; // AB_Pges Gauge var chartAB_Pges = Highcharts.chart('container-AB_Pges', Highcharts.merge(gaugeOptions, { pane: { size: '150%' }, yAxis: { min: 0, max: 6000, title: { y: -80, style: { font: 'bold 16px Dosis, sans-serif', color: '#000000', }, text: 'Gesamtwirkleistung' } }, series: [{ name: 'AB_Pges', data: [0], dataLabels: { format: '<div style="text-align:center"><span style="font-size:30px;' + ((Highcharts.theme && Highcharts.theme.contrastTextColor) || 'black') + '">{y}<\/span><br>' + '<span style="font-size:22px;color:silver">W<\/span><\/div>' }, }] })); // AB_PL1 Gauge var chartAB_PL1 = Highcharts.chart('container-AB_PL1', Highcharts.merge(gaugeOptions, { yAxis: { min: 0, max: 2000, title: { y: -50, style: { font: 'bold 16px Dosis, sans-serif', color: '#000000', }, text: 'Wirkleistung L1' } }, series: [{ name: 'AB_PL1', data: [0], dataLabels: { format: '<div style="text-align:center"><span style="font-size:20px;' + ((Highcharts.theme && Highcharts.theme.contrastTextColor) || 'black') + '">{y}<\/span><br>' + '<span style="font-size:16px;color:silver">W<\/span><\/div>' }, tooltip: { valueSuffix: ' W' } }] })); // AB_PL2 gauge var chartAB_PL2 = Highcharts.chart('container-AB_PL2', Highcharts.merge(gaugeOptions, { yAxis: { min: 0, max: 2000, title: { y: -50, style: { font: 'bold 16px Dosis, sans-serif', color: '#000000', }, text: 'Wirkleistung L2' } }, series: [{ name: 'AB_PL2', data: [0], dataLabels: { format: '<div style="text-align:center"><span style="font-size:20px;' + ((Highcharts.theme && Highcharts.theme.contrastTextColor) || 'black') + '">{y}<\/span><br>' + '<span style="font-size:16px;color:silver">W<\/span><\/div>' }, }] })); // AB_PL3 gauge var chartAB_PL3 = Highcharts.chart('container-AB_PL3', Highcharts.merge(gaugeOptions, { yAxis: { min: 0, max: 2000, title: { y: -50, style: { font: 'bold 16px Dosis, sans-serif', color: '#000000', }, text: 'Wirkleistung L3' } }, series: [{ name: 'AB_PL3', data: [0], dataLabels: { format: '<div style="text-align:center"><span style="font-size:20px;' + ((Highcharts.theme && Highcharts.theme.contrastTextColor) || 'black') + '">{y}<\/span><br>' + '<span style="font-size:16px;color:silver">W<\/span><\/div>' }, }] })); // WP_Pges gauge var chartWP_Pges = Highcharts.chart('container-WP_Pges', Highcharts.merge(gaugeOptions, { pane: { size: '150%' }, yAxis: { min: 0, max: 2000, title: { y: -80, style: { font: 'bold 16px Dosis, sans-serif', color: '#000000', }, text: 'Gesamtwirkleistung' } }, series: [{ name: 'WP_Pges', data: [0], dataLabels: { format: '<div style="text-align:center"><span style="font-size:30px;color:' + ((Highcharts.theme && Highcharts.theme.contrastTextColor) || 'black') + '">{point.y:,.1f}<\/span><br>' + '<span style="font-size:22px;color:silver">W<\/span><\/div>' }, }] })); // JSON abholen setInterval(function () { $.ajax({ type: "GET", url: "/cgi-bin/get_AB_json.py", // error: function() { // alert("Script konnte nicht ausgeführt werden"); //}, success: function(data, status){ var response = JSON.parse(data); var point, newVal, inc; if (chartAB_Pges) { point = chartAB_Pges.series[0].points[0]; newVal = response.AB_Pges; point.update(newVal); } if (chartAB_PL1) { point = chartAB_PL1.series[0].points[0]; newVal = response.AB_PL1; point.update(newVal); } if (chartAB_PL2) { point = chartAB_PL2.series[0].points[0]; newVal = response.AB_PL2; point.update(newVal); } if (chartAB_PL3) { point = chartAB_PL3.series[0].points[0]; newVal = response.AB_PL3; point.update(newVal); } AB_Wges = (response.AB_Wges).toFixed(4); str_AB_Wges = ('<center><div class="segfontbk">' + AB_Wges.split(".")[0] + '<\/div><div class="komma">,<\/div><div class="segfontbk">' + AB_Wges.split(".")[1] + '<\/div>kWh<\/center>'); document.getElementById("container-AB_Wges").innerHTML = str_AB_Wges; } }); $.ajax({ type: "GET", url: "/cgi-bin/get_WP_json.py", //error: function() { // alert("Script 2 konnte nicht ausgeführt werden"); //}, success: function(data, status){ var response = JSON.parse(data); var point, newVal, inc; if (chartWP_Pges) { point = chartWP_Pges.series[0].points[0]; newVal = response.WP_Pges; point.update(newVal); } WP_Wges = (response.WP_Wges).toFixed(4); str_WP_Wges = ('<center><div class="segfontbk">' + WP_Wges.split(".")[0] + '<\/div><div class="komma">,<\/div><div class="segfontbk">' + WP_Wges.split(".")[1] + '<\/div>kWh<\/center>'); document.getElementById("container-WP_Wges").innerHTML = str_WP_Wges; } }); }, 2000); </script> </div> </body> </html>