#!/usr/bin/python
# -*- coding: latin-1 -*-

# Copyright: Thomas Wagner
# Siehe auch
# http://www.schatenseite.de/2016/05/30/smart-message-language-stromzahler-auslesen/
# https://www.msxfaq.de/sonst/bastelbude/smartmeter_d0_sml_protokoll.htm
# https://www.bsi.bund.de/SharedDocs/Downloads/DE/BSI/Publikationen/TechnischeRichtlinien/TR03109/TR-03109-1_Anlage_Feinspezifikation_Drahtgebundene_LMN-Schnittstelle_Teilb.pdf?__blob=publicationFile


import platform
import os, sys
import datetime
from hexdump import hexdump
import copy

# Softwareversion
Version= "0.95"

# Globale Parameter
verboselevel = 0 # Keine Zusatzmeldungen
#verboselevel = 1 # Alle Debugmeldungen anzeigen

smlbreite = 51      # Breite der SML HEX-Ausgabe
decodebreite = 21   # Breite der SML ASCII Ausgabe

def nibble_byte(Byte):
    high, low = Byte >> 4, Byte & 0x0F
    return high, low

def byar2hex(bytearr):
    return " ".join((format(x, '02X') for x in bytearr))

def printv(string=''):
    if verboselevel == 0:
        pass
    else:
        print(string)
        
def hex2ascii(string):
    if string[0]=="-" or "Anzahl der SML-Nachrichten" in string:
        return ""
    string=string.strip(" ").split(" ")
    Ergebnis=""
    if string[0][0]=="7":
        Ergebnis="Liste (" + string[0][1]+ " Elemente)"
    elif string[0]=="01" and len(string)==1:
        Ergebnis= "      (leer)"
    elif string[0]=="00" and len(string)==1:
        Ergebnis= "      (Ende)"        
    elif string[0][0]=="6":
        string="".join(string)
        if string[1]=="2":
            Ergebnis= "UInt8:  "+str(int(string[2:],16))
        elif string[1]=="3":
            Ergebnis= "UInt16: "+str(int(string[2:],16))
        elif string[1]=="5":
            Ergebnis= "UInt32: "+str(int(string[2:],16))
        elif string[1]=="9":
            Ergebnis= "UInt64: "+str(int(string[2:],16))
    elif string[0][0]=="5":
        string="".join(string)
        if string[1]=="2":
            Ergebnis= "Int8:   "+str(int(string[2:],16))
        elif string[1]=="3":
            Ergebnis= "Int16:  "+str(int(string[2:],16))
        elif string[1]=="5":
            Ergebnis= "Int32:  "+str(int(string[2:],16))
        elif string[1]=="9":
            Ergebnis= "Int64:  "+str(int(string[2:],16))            
    else:
        Ergebnis = "   "
        for l in string:
            i= int(l, 16)
            if i < 32 or i > 126:
                Ergebnis +="."
            else:
                Ergebnis += chr(int(l, 16))
    return Ergebnis

def intdecode(string):
    if string[0][0]=="6":
        Ergebnis= int(string[3][2:].replace(" ",""),16)
    elif string[0][0]=="5":
        Ergebnis= int(string[3][2:].replace(" ",""),16)
    return Ergebnis/10000

def Alle_elementaren_Listen(Liste):
    #Gibt eine Liste aller Listen der Struktur zurück, die keine weiteren Unterlisten enthalten
    
    Ergebnisliste=[]
    
    # Ist die Liste elementar?
    ElementareListe=True
    for Befehl in Liste[3]:
        if Befehl[1] == 7: #Befehl ist eine Liste, in diesem Fall Befehl rekursiv ausführen und Ergebnis anhängen
            ElementareListe=False
            Ergebnisliste +=Alle_elementaren_Listen(Befehl)

    # Falls die Liste elementar ist, an die Ergebnisliste anhängen
    if ElementareListe:
        Ergebnisliste.append(Liste)
        
    return Ergebnisliste
    

class SML_Element():
    def __init__(self, Restdaten):
        self.Restdaten = copy.deepcopy(Restdaten)

        # Führendes Element abspalten
        Typ, Elementlaenge = nibble_byte(self.Restdaten[0])
        LeadByte = format(self.Restdaten[0], '02X')
        if Typ == 0 and Elementlaenge==0: 
            Elementlaenge=1
            printv("Typdefinition: "+LeadByte)
            printv("Typ Oktett (Schlussbyte), Elementlänge: "+str(Elementlaenge))
            Nutzdaten_Element=copy.deepcopy(self.Restdaten[1:int(Elementlaenge)])
            self.Restdaten=copy.deepcopy(self.Restdaten[int(Elementlaenge):])
            printv("Nutzdaten Element: ")
            printv(hexdump(Nutzdaten_Element, result='return'))
            printv("Restdaten:")
            printv(hexdump(self.Restdaten, result='return'))
            printv("Länge: "+str(len(self.Restdaten)))
            printv() 
            self.Element=[LeadByte, Typ, Elementlaenge, byar2hex(Nutzdaten_Element)]
            printv(self.Element)
            printv()
        elif Typ == 0: #Octettstring
            printv("Typdefinition: "+LeadByte)
            printv("Typ Oktett, Elementlänge: "+str(Elementlaenge))
            Nutzdaten_Element=copy.deepcopy(self.Restdaten[1:int(Elementlaenge)])
            self.Restdaten=copy.deepcopy(self.Restdaten[int(Elementlaenge):])
            printv("Nutzdaten Element: ")
            printv(hexdump(Nutzdaten_Element, result='return'))
            printv("Restdaten:")
            printv(hexdump(self.Restdaten, result='return'))
            printv("Länge: "+str(len(self.Restdaten)))
            printv() 
            self.Element=[LeadByte, Typ, Elementlaenge, byar2hex(Nutzdaten_Element)]
            printv(self.Element)
            printv()
        elif Typ == 4: # Booplean (S. 42 BSI) : 
            if Elementlaenge ==2:
                Typstring="Boolean"
            else:
                print("Fehler bei Boolean-Analyse")
                sys.exit("Abbruch")
            printv("Typ "+Typstring+ ", Elementlänge: "+str(Elementlaenge))
            Nutzdaten_Element=copy.deepcopy(self.Restdaten[1:int(Elementlaenge)])
            self.Restdaten=copy.deepcopy(self.Restdaten[int(Elementlaenge):])
            printv("Nutzdaten Element: ")
            printv(hexdump(Nutzdaten_Element, result='return'))
            printv("Restdaten:")
            printv(hexdump(self.Restdaten, result='return'))
            printv("Länge: "+str(len(self.Restdaten)))
            printv() 
            self.Element=[LeadByte, Typ, Elementlaenge, byar2hex(Nutzdaten_Element)]
            printv(self.Element)
            printv()             
        elif Typ == 5: # Integer (S. 34 BSI) : 52: Int8, 53: Int16,  55: Int32, 59: Int64
            if Elementlaenge ==2:
                Typstring="Integer8"
            elif Elementlaenge ==3:
                Typstring="Integer16"
            elif Elementlaenge ==5:
                Typstring="Integer32"
            elif Elementlaenge ==9:
                Typstring="Integer64"
            else:
                print("Fehler bei Integer-Analyse")
                sys.exit("Abbruch")
            printv("Typ "+Typstring+ ", Elementlänge: "+str(Elementlaenge))
            Nutzdaten_Element=copy.deepcopy(self.Restdaten[1:int(Elementlaenge)])
            self.Restdaten=copy.deepcopy(self.Restdaten[int(Elementlaenge):])
            printv("Nutzdaten Element: ")
            printv(hexdump(Nutzdaten_Element, result='return'))
            printv("Restdaten:")
            printv(hexdump(self.Restdaten, result='return'))
            printv("Länge: "+str(len(self.Restdaten)))
            printv() 
            self.Element=[LeadByte, Typ, Elementlaenge, byar2hex(Nutzdaten_Element)]
            printv(self.Element)
            printv() 
        elif Typ == 6: # Unsigned Integer (S. 35 BSI) : 62: UInt8, 63: Unt16,  65: Unt32, 69: Unt64
            if Elementlaenge ==2:
                Typstring="UnsignedInteger8"
            elif Elementlaenge ==3:
                Typstring="UnsignedInteger16"
            elif Elementlaenge ==5:
                Typstring="UnsignedInteger32"
            elif Elementlaenge ==9:
                Typstring="UnsignedInteger64"
            else:
                print("Fehler bei Unsigned Integer-Analyse")
                sys.exit("Abbruch")
            printv("Typ "+Typstring+ ", Elementlänge: "+str(Elementlaenge))
            Nutzdaten_Element=copy.deepcopy(self.Restdaten[1:int(Elementlaenge)])
            self.Restdaten=copy.deepcopy(self.Restdaten[int(Elementlaenge):])
            printv("Nutzdaten Element: ")
            printv(hexdump(Nutzdaten_Element, result='return'))
            printv("Restdaten:")
            printv(hexdump(self.Restdaten, result='return'))
            printv("Länge: "+str(len(self.Restdaten)))
            printv() 
            self.Element=[LeadByte, Typ, Elementlaenge, byar2hex(Nutzdaten_Element)]
            printv(self.Element)
            printv() 
        elif Typ == 7:
            printv("Typdefinition: "+LeadByte)
            printv("Typ Liste, Listenlänge: "+str(Elementlaenge))
            self.Nutzdaten=copy.deepcopy(self.Restdaten[1:])
            printv("Nutzdaten Liste: ")
            printv(hexdump(self.Nutzdaten, result='return'))
            printv("Länge: "+str(len(self.Nutzdaten)))
            printv()
            self.Element=[LeadByte, Typ, Elementlaenge]
            printv(self.Element)
            printv()
            
            # Alle Elemente isolieren
            self.Restdaten=copy.deepcopy(self.Nutzdaten)
            self.Elemente=[]
            
            for i in range(Elementlaenge):
                NaechstesElement=SML_Element(self.Restdaten)
                self.Elemente.append(NaechstesElement.Element)
                self.Restdaten=copy.deepcopy(NaechstesElement.Restdaten)
                printv("Nächstes Element analysiert: ------------------------------")
                printv(self.Elemente)
                printv()
 
            self.Element.append(self.Elemente)
            printv(self.Element)
            printv()
        elif Typ == 8 and int(self.Restdaten[1])<8:  # Überlanger Oktettstring mit zweitem TL Byte
            Elementlaenge =Elementlaenge*16 +int(self.Restdaten[1])
            LeadByte =LeadByte +" " +str(self.Restdaten[1])
            printv("Typdefinition: "+LeadByte)
            printv("Typ Oktett, Elementlänge: "+str(Elementlaenge))
            Nutzdaten_Element=copy.deepcopy(self.Restdaten[2:int(Elementlaenge)])
            self.Restdaten=copy.deepcopy(self.Restdaten[int(Elementlaenge):])
            printv("Nutzdaten Element: ")
            printv(hexdump(Nutzdaten_Element, result='return'))
            printv()
            printv("Restdaten:")
            printv(hexdump(self.Restdaten, result='return'))
            printv("Länge: "+str(len(self.Restdaten)))
            printv() 
            self.Element=[LeadByte, Typ, Elementlaenge, byar2hex(Nutzdaten_Element)]
            printv(self.Element)
            printv()            
        else:
            print("Restdaten:")
            print(hexdump(self.Restdaten, result='return'))
            print("Analysefehler Element")   
            sys.exit("Angehalten wegen Fehler in Analyse")
            
        if self.Element==[]:
            print()    
            print("Analysefehler Element leer")   
            sys.exit("Angehalten wegen Fehler in Analyse")            
    
        
class SML():
    def __init__(self, Filename):
        #Filename ist überladen: Als String wird es als Filename interpretiert, als Binärdaten (type: bytes) direkt als Datensatz
        self.Marker={'Start': b'\x1B\x1B\x1B\x1B\x01\x01\x01\x01', \
                'Ende': b'\x1B\x1B\x1B\x1B\x1A', \
                '1.8.0': b'\x77\x07\x01\x00\x01\x08\x00', \
                '2.8.0': b'\x77\x07\x01\x00\x02\x08\x00', \
                'aktLeist': b'\x77\x07\x01\x00\x01\x07\x00', \
                'Zaehlerstellenstart': b'\x77\x07\x01\x00', \
                }
        # Wurde Filename oder Binärdaten übergeben?
        if isinstance(Filename, str): #es wurde ein Filename übergeben
            # File in die Klasse einlesen
            with open(Filename, "rb") as binary_file:
                # Read the whole file at once
                self.Fileinhalt = binary_file.read()
                # Filelänge bestimmen    
                self.Filelaenge = os.path.getsize(Filename)
        else: #es wurden direkt Daten übergeben
            # Read the whole file at once
            self.Fileinhalt = Filename
            # Filelänge bestimmen    
            self.Filelaenge = len(self.Fileinhalt)
        
        # Erste Sequenz isolieren:
        Pos_Start=self.Fileinhalt.find(self.Marker['Start'])
        Pos_Ende=self.Fileinhalt[Pos_Start:].find(self.Marker['Ende'])+Pos_Start
        self.Sequenz=self.Fileinhalt[Pos_Start:(Pos_Ende+len((self.Marker['Ende'])))]
        
        print("Erste Datensequenz:")
        printv(hexdump(self.Sequenz, result='return'))
        printv("Länge ersten Datensequenz: "+str(len(self.Sequenz)))
        printv()
        
        # Nutzdaten insolieren
        if self.Sequenz[0:8]== b'\x1B\x1B\x1B\x1B\x01\x01\x01\x01' and self.Sequenz[-5:]== b'\x1B\x1B\x1B\x1B\x1A':
            printv("Escape Sequenz an Position: 0 - 3")
            printv("Start Version 1.0 an Position: 4 - 7")
            printv(hexdump(self.Sequenz[0:8], result='return'))
            printv()
            printv("Endsequenz: :-5")
            printv(hexdump(self.Sequenz[-5:], result='return'))
            printv()
            
            self.Nutzdaten=self.Sequenz[8:-5]
            print("Nutzdaten")
            print(hexdump(self.Nutzdaten, result='return'))
            print("Länge Nutzdaten: "+str(len(self.Nutzdaten)))
            print()
        else:
            print("Analysefehler Startsequenz")
            sys.exit("Angehalten wegen Fehler in Analyse")

        self.Struktur=[]
        # Gibts noch eine Liste?    
        while len(self.Nutzdaten)>0: 
            temp=SML_Element(self.Nutzdaten)
            self.Struktur.append(temp.Element)
            self.Nutzdaten=copy.deepcopy(temp.Restdaten)
            printv(hexdump(self.Nutzdaten, result='return'))
            printv("Länge Nutzdaten: "+str(len(self.Nutzdaten)))
            printv()
        
        
        
    def Drucken(self):
        global Indent 
        Indent=0
        global DIndent
        DIndent=3

        
        def Element2String(List):
            global Indent
            global DIndent
            
            Ergebnis=""
            #printv(List)
            if not isinstance(List[0], list) and List[1]==7 : # Eine Liste
                Ergebnis += " "*Indent*DIndent+List[0]+"\n"
                Indent +=1
                for i in range(List[2]):
                    if not isinstance(List[3][i], list):
                        Ergebnis += " "*Indent*DIndent+List[3][i]+"\n"
                    else:
                        Ergebnis +=Element2String(List[3][i])
                Indent -=1
            elif not isinstance(List[0], list) and List[1]==0 : # Ein Oktett
                Ergebnis += " "*Indent*DIndent+List[0] +" "+ List[3]+"\n"
            elif not isinstance(List[0], list) and List[1]==5 : # Integer
                Ergebnis += " "*Indent*DIndent +List[0] +" "+ List[3]+"\n"
            elif not isinstance(List[0], list) and List[1]==6 : # Unsigned Integer
                Ergebnis += " "*Indent*DIndent +List[0] +" "+ List[3]+"\n"
            elif not isinstance(List[0], list) and List[1]==8 : # Überlanges Oktett mit zweitem TL Byte
                Ergebnis += " "*Indent*DIndent +List[0] +" "+ List[3]+"\n"
            else:
                print(List)
                print()
                print(List[0], List[1])
                print("Fehler beim Druck")
                sys.exit("Angehalten wegen Fehler in Analyse")
            return Ergebnis
                
        
        print()
        print("______________________________ Ergebnisstruktur ____________________")
        print()
        printv(self.Struktur)
        printv()
        printv()
        Ausgabe=""
        Ausgabe += "------------------- Beginn der SML-Datei ---------\n"
        Ausgabe += "Anzahl der SML-Nachrichten in SML-Datei: "+str(len(self.Struktur))+"\n"
        for i in range(len(self.Struktur)):
            Ausgabe += "---------------- SML-Nachricht "+str(i)+ " ------------"+"\n"
            Ergebnis=Element2String(self.Struktur[i])
            Ausgabe += Ergebnis
        Ausgabe += "------------------- Ende der SML-Datei -----------" +"\n"
        
        Ausgabe=Ausgabe.rstrip().split("\n")
        AusgabeNeu=""
        for line in Ausgabe:
            AusgabeNeu += line + " "*(smlbreite-len(line)) + hex2ascii(line) + " "*(decodebreite-len(hex2ascii(line))) +"\n"
        print(AusgabeNeu)            
        print()
        
    def OBIS_Alle(self):
        # Bisherige Suchstrategie
        # + Stecken immer in Nachricht 1 (Start 0)
        # + Nur Listen ohne Unterlisten betrachten
        # + Immer eine 77 er Liste, in der erster Befehl 07 ist und mit 01 00 anfängt
        # + Obis Kennzahl ist im nullten Listenelement (Start 0), Zählerstand im 5.ten)
        # + Erste Ziffer der Obis Kenntzahlen muss kleiner als 10 sein
        # + Zählerstand kann unterschiedlich lang sein
                    
        Nachricht = self.Struktur[1] #Stecken immer in Nachricht 1 (Start 0)
        Ergebnis=Alle_elementaren_Listen(Nachricht) #Nur Listen ohne Unterlisten betrachten
        
        
        print("-------- OBIS-Kennzahlen in der SML-Datei --------")
        
        self.OBIS={}
        for Liste in Ergebnis:
            if Liste[0]=="77": #Immer eine 77 er Liste
                try:
                    if Liste[3][0][0]=="07" and Liste[3][0][3][:5]=="01 00": # in der erstes Element 07 ist und mit 01 00 anfängt
                        temp= Liste[3][0][3][6:14].split(" ")
                        if int(temp[0]) <10:
                            OBISNr=str(int(temp[0])) +"."+ str(int(temp[1])) +"."+str(int(temp[2]))
                            OBISWert=intdecode(Liste[3][5])
                            print("Obis-Kennzahl: " + OBISNr+ "  Wert: "+str(OBISWert))
                            self.OBIS[OBISNr]=OBISWert
                except:
                    pass
        return self.OBIS
    
    def Zaehlerinfos_Alle(self):
        # Bisherige Suchstrategie
        # + Steckt immer in Nachricht 0 (Start 0)
        # + Nur Listen ohne Unterlisten betrachten
        # + Immer die einzige elementare 76 er Liste, in der 1. und 2. Befehl 01 ist 
        # + Zählerinfos im 2. und 3. Listenelement (Start 0)
                    
        Nachricht = self.Struktur[0] #Stecken immer in Nachricht 0 (Start 0)
        Ergebnis=Alle_elementaren_Listen(Nachricht) #Nur Listen ohne Unterlisten betrachten
        
        
        for Liste in Ergebnis:
            if Liste[0]=="76": #Immer eine 76 er Liste
                try:
                    if Liste[3][0][0]=="01" and Liste[3][1][0]=="01": # erster und zweiter Befehl der Liste gleich 01
                        print("-------- Zählerinfos -----------------------------")
                        print(Liste[3][2][3])
                        print(hex2ascii(Liste[3][2][3]))
                        print(Liste[3][3][3])
                        print(hex2ascii(Liste[3][3][3]))
                        print()
                        print()
                        self.Zaehlerinfos=[hex2ascii(Liste[3][2][3]), hex2ascii(Liste[3][3][3])]
                        return self.Zaehlerinfos
                except:
                    pass
        return 
    


if __name__ == '__main__':
    # Parameter
    Filename="feltgen1.hex"
    Filename="macha.hex" 
    
    print("Direkter Aufruf")
    SML1=SML(Filename)
    SML1.Drucken()
    SML1.Zaehlerinfos_Alle()
    SML1.OBIS_Alle()
    #print(SML1.OBIS['1.8.0'])
    #print(SML1.Zaehlerinfos_Alle())
    print("Programmende")
    
    
    
        
