Das SML-Interface


SML-Interface-Platine oben SML-Interface-Platine unten

Vorwort

Aufgrund eines Feedbacks zu meiner SML-Seite möchte ich hier jetzt ein paar Worte vorwegschicken.
  1. 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.
  2. 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.
  3. Vermutlich sind auch Änderungen notwendig, wenn eingespeißt werden sollte. -> Stichwort Zweierkomplement
  4. Es gibt Zähler mit dynamischen Längen der Listeneinträge, auch das beherrschen meine Scripte nicht!
  5. 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.
  6. 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.

Schaltplan SML-Interface


Löten

Das Löten ging aufgrund der wenigen Bauteile schnell von der Hand.

Platinen aus Fernost Platine IR-Seite
Platine USB-Seite Platine USB-Seite


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:

Gehäuse1 Gehäuse2 Gehäuse3

und im Schrank:

eingebaut


Stückliste

BauteilBestellnummerMengeEinzelpreisGesamtpreis
TME
FototransistorSFH309FA-410,62 €0,62 €
Schmitt-TriggerSN74LVC1G17DBV10,10 €0,10 €
KnickschutztülleH125-HV-BK-M110,17 €0,17 €
IR-Sendediode
alternative
OSI3CA3131A10,13 €0,13 €
Reichelt
IR-Sendediode
abgekündigt!!!
SEP 8705-00311,50 €1,50 €
USB ICFT 232 RL13,60 €3,60 €
npn-TransistorBC84610,04 €0,04 €
pnp-TransistorBCX1710,05 €0,05 €
Widerstand 0805 270R
R4 für SEP 8705-003
SMD-0805 27010,03 €0,03 €
Widerstand 0805 1K
R4 für OSI3CA3131A
SMD-0805 1,00K10,03 €0,03 €
Widerstand 0805 390RSMD-0805 39020,03 €0,06 €
Widerstand 0805 12kSMD-0805 12,0K30,03 €0,09 €
Kerko 0805 100nX7R-G0805 100N20,03 €0,06 €
Kondensator Tantal 22μFSMD TAN .22/2010,26 €0,26 €
USB KabelAK 670/2-3,011,35 €1,35 €
Voelkner / Conrad
SMD LED 1206 rot15631410,20 €0,20 €
SMD LED 1206 grün15631510,20 €0,20 €
Supermagnete.de
RingmagnetR-27-16-05-N12,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:
  1. 1b 1b 1b 1b #Escape Sequenz
  2. 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.
  1. 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.
  1. 05 03 2b 18 0f #0=Octet String 5=Byte Länge -> 1. Eintrag in Liste 1
  2. 62 00 #2. Eintrag in Liste 1
  3. 62 00 #3. Eintrag in Liste 1
  4. 72 #4. Eintrag in Liste 1 -> neue Liste 2
  5. 63 01 01 #1. Eintrag in Liste 2
  6. 76 #2. Eintrag in Liste 2 -> neue Liste 3
  7. 01 #1. Eintrag in Liste 3
  8. 01 #2. Eintrag in Liste 3
  9. 05 01 0e 5d 5b #3. Eintrag in Liste 3
  10. 0b XX XX XX XX XX XX XX XX XX XX #4. Eintrag in Liste 3
  11. 01 #5. Eintrag in Liste 3
  12. 01 #6. Eintrag in Liste 3
  13. 63 49 00 #5. Eintrag in Liste 1
  14. 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!!!
  1. 1b 1b 1b 1b #Escape Sequenz
  2. 01 01 01 01 #Start Version 1.0
  3. 76 #Liste mit 6 Einträgen (1. SML Nachricht in diesem Telegramm)
  4. 05 03 2b 18 0f #transactionId:
  5. 62 00 #groupNo:
  6. 62 00 #abortOnError:
  7. 72 #messageBody: Liste mit 2 Einträgen
  8. 63 01 01 #getOpenResponse:
  9. 76 #Liste mit 6 Einträgen
  10. 01 #codepage: ohne Wert
  11. 01 #clientId: ohne Wert
  12. 05 01 0e 5d 5b #reqFileId:
  13. 0b XX XX XX XX XX XX XX XX XX XX #serverID: (0b(HEX)=11Bytes Länge)
  14. 01 #refTime: ohne Wert
  15. 01 #smlVersion: ohne Wert
  16. 63 49 00 #CRC Prüfsumme der ersten Nahricht
  17. 00 #Ende der ersten SML Nachricht
  18. 76 #Liste mit 6 Einträgen (2. SML Nachricht in diesem Telegramm)
  19. 05 03 2b 18 10 #transactionId:
  20. 62 00 #groupNo:
  21. 62 00 #abortOnError:
  22. 72 #messageBody: Liste mit 2 Einträgen
  23. 63 07 01 #getListResponse:
  24. 77 #Liste mit 7 Einträgen
  25. 01 #clientId: ohne Wert
  26. 0b XX XX XX XX XX XX XX XX XX XX #serverID:
  27. 07 01 00 62 0a ff ff #ListName:
  28. 72 #Liste mit 2 Einträgen
  29. 62 01 #choice: secIndex
  30. 65 02 1a 58 7f #secIndex: (Uptime)
  31. 7d #Liste mit 11 Einträgen
  32. 77 #Liste mit 7 Einträgen
  33. 07 81 81 c7 82 03 ff #objName: OBIS Kennzahl für den Hersteller
  34. 01 #status: ohne Wert
  35. 01 #valTime: ohne Wert
  36. 01 #unit: ohne Wert
  37. 01 #scaler: ohne Wert
  38. 04 49 53 4b #value: (49 53 4b)Ascii = ISK
  39. 01 #valueSignature: ohne Wert
  40. 77 #Liste mit 7 Einträgen
  41. 07 01 00 00 00 09 ff #objName: OBIS Kennzahl für Gerätenummer = ServerID
  42. 01 #status: ohne Wert
  43. 01 #valTime: ohne Wert
  44. 01 #unit: ohne Wert
  45. 01 #scaler: ohne Wert
  46. 0b XX XX XX XX XX XX XX XX XX XX #value: Wert für Gerätenummer
  47. 01 #valueSignature: ohne Wert
  48. 77 #Liste mit 7 Einträgen
  49. 07 01 00 01 08 00 ff #objName: OBIS Kennzahl für Wirkenergie Bezug gesamt tariflos
  50. 65 00 01 01 80 #status ?
  51. 01 #valTime: ohne Wert
  52. 62 1e #unit: 1e = "Wh"
  53. 52 ff #scaler: ff(integer)=-1(dezimal) -> 10^-1 = 0.1
  54. 59 00 00 00 00 01 c9 0c a7 #unit: Wert für Wirkenergie Bezug gesamt tariflos
  55. 01 #valueSignature: ohne Wert
  56. 77 #Liste mit 7 Einträgen
  57. 07 01 00 01 08 01 ff #objName: OBIS-Kennzahl für Wirkenergie Bezug Tarif1
  58. 01 #status: ohne Wert
  59. 01 #valTime: ohne Wert
  60. 62 1e #unit: 1e = "Wh"
  61. 52 ff #scaler: ff(integer)=-1(dezimal) -> 10^-1 = 0.1
  62. 59 00 00 00 00 01 c9 0c a7 #unit: Wert für für Wirkenergie Bezug Tarif1
  63. 01 #valueSignature: ohne Wert
  64. 77 #Liste mit 7 Einträgen
  65. 07 01 00 01 08 02 ff #objName: OBIS-Kennzahl für Wirkenergie Bezug Tarif2
  66. 01 #status: ohne Wert
  67. 01 #valTime: ohne Wert
  68. 62 1e #unit: 1e = "Wh"
  69. 52 ff #scaler: ff(integer)=-1(dezimal) -> 10^-1 = 0.1
  70. 59 00 00 00 00 00 00 00 00 #value: Wert für Wirkenergie Bezug Tarif2
  71. 01 #valueSignature: ohne Wert
  72. 77 #Liste mit 7 Einträgen
  73. 07 01 00 02 08 00 ff #objName: OBIS-Kennzahl für Wirkenergie Einspeisung gesamt tariflos
  74. 01 #status: ohne Wert
  75. 01 #valTime: ohne Wert
  76. 62 1e #unit: 1e = "Wh"
  77. 52 ff #scaler: ff(integer)=-1(dezimal) -> 10^-1 = 0.1
  78. 59 00 00 00 00 00 00 00 00 #value: Wert für Wirkenergie Einspeisung gesamt tariflos
  79. 01 #valueSignature: ohne Wert
  80. 77 #Liste mit 7 Einträgen
  81. 07 01 00 02 08 01 ff #objName: OBIS-Kennzahl für Wirkenergie Einspeisung Tarif1
  82. 01 #status: ohne Wert
  83. 01 #valTime: ohne Wert
  84. 62 1e #unit: 1e = "Wh"
  85. 52 ff #scaler: ff(integer)=-1(dezimal) -> 10^-1 = 0.1
  86. 59 00 00 00 00 00 00 00 00 #value: Wert für Wirkenergie Einspeisung Tarif1
  87. 01 #valueSignature: ohne Wert
  88. 77 #Liste mit 7 Einträgen
  89. 07 01 00 02 08 02 ff #objName: OBIS-Kennzahl für Wirkenergie Einspeisung Tarif2
  90. 01 #status: ohne Wert
  91. 01 #valTime: hier ohne Wert
  92. 62 1e #unit: 1e = "Wh"
  93. 52 ff #scaler: ff(integer)=-1(dezimal) -> 10^-1 = 0.1
  94. 59 00 00 00 00 00 00 00 00 #value: Wert für Wirkenergie Einspeisung Tarif2
  95. 01 #valueSignature: ohne Wert
  96. 77 #Liste mit 7 Einträgen
  97. 07 01 00 10 07 00 ff #objName: OBIS-Kennzahl für momentane Gesamtwirkleistung
  98. 01 #status: ohne Wert
  99. 01 #valTime: ohne Wert
  100. 62 1b #unit: 1b = "W"
  101. 52 00 #scaler: 0(integer)=0(dezimal) -> 10^0 = 1
  102. 55 00 00 00 5b #value: Wert für momentane Gesamtwirkleistung
  103. 01 #valueSignature: ohne Wert
  104. 77 #Liste mit 7 Einträgen
  105. 07 01 00 24 07 00 ff #objName: OBIS-Kennzahl für momentane Wirkleistung in Phase L1
  106. 01 #status: ohne Wert
  107. 01 #valTime: ohne Wert
  108. 62 1b #unit: 1b = "W"
  109. 52 00 #scaler: 0(integer)=0(dezimal) -> 10^0 = 1
  110. 55 00 00 00 17 #value: Wert für momentane Wirkleistung in Phase L1
  111. 01 #valueSignature: ohne Wert
  112. 77 #Liste mit 7 Einträgen
  113. 07 01 00 38 07 00 ff #objName: OBIS-Kennzahl für momentane Wirkleistung in Phase L2
  114. 01 #status: ohne Wert
  115. 01 #valTime: ohne Wert
  116. 62 1b #unit: 1b = "W"
  117. 52 00 #scaler: 0(integer)=0(dezimal) -> 10^0 = 1
  118. 55 00 00 00 2e #value: Wert für momentane Wirkleistung in Phase L2
  119. 01 #valueSignature: ohne Wert
  120. 77 #Liste mit 7 Einträgen
  121. 07 01 00 4c 07 00 ff #objName: OBIS-Kennzahl für momentane Wirkleistung in Phase L3
  122. 01 #status: ohne Wert
  123. 01 #valTime: ohne Wert
  124. 62 1b #unit: 1b = "W"
  125. 52 00 #scaler: 0(integer)=0(dezimal) -> 10^0 = 1
  126. 55 00 00 00 15 #value: Wert für momentane Wirkleistung in Phase L2
  127. 01 #valueSignature: ohne Wert
  128. 77 #Liste mit 7 Einträgen
  129. 07 81 81 c7 82 05 ff #objName: OBIS-Kennzahl für publicKey
  130. 01 #status: ohne Wert
  131. 01 #valTime: ohne Wert
  132. 01 #unit: ohne Wert
  133. 01 #scaler: ohne Wert
  134. 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
  135. 01 #valueSignature: ohne Wert
  136. 01 #listSignature: ohne Wert
  137. 01 #actGatewayTime: ohne Wert
  138. 63 54 86 #CRC Prüfsumme der zweiten Nachricht
  139. 00 #Ende der zweiten SML Nachricht
  140. 76 #7=Liste 6=Anzahl der Einträge (3. SML Nachricht in diesem Telegramm)
  141. 05 03 2b 18 11 #transactionId:
  142. 62 00 #groupNo:
  143. 62 00 #abortOnError:
  144. 72 #messageBody: Liste mit 2 Einträgen
  145. 63 02 01 #getCloseResponse:
  146. 71 #Liste mit einem Eintrag
  147. 01 #? ohne Wert
  148. 63 fa 36 #CRC Prüfsumme der dritten Nachricht
  149. 00 #Ende der dritten SML Nachricht
  150. 1b 1b 1b 1b #Escape Sequenz
  151. 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
Prüft man die CRC auf dieser Seite stellt man fest, dass die beiden Bytes im Ergebnis vertauscht sind. Der Einfachheit halber vertausche ich die beiden Bytes aus dem SML-Telegram vor dem Vergleich. Später bin ich von der Prüfung mit dem Polynom abgekommen und nutze jetzt stattdessen eine Lookup-Tabelle.


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.py
Folgenden 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.py
Nun 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/fstab
In 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>



Ergebnis

So könnte das Ganze dann aussehen. Da ich die IP meines Home-Servers nicht veröffentlichen möchte, werden im folgendem iFrame nur zufällige Dummywerte angezeigt. Eine JSON-Übertragung findet hier also nicht statt.