├── LICENSE ├── README.md ├── assistant.py ├── e3dc ├── __init__.py ├── __pycache__ │ ├── __init__.cpython-37.pyc │ ├── __init__.cpython-38.pyc │ ├── _rscp_dto.cpython-37.pyc │ ├── _rscp_dto.cpython-38.pyc │ ├── _rscp_encrypt_decrypt.cpython-37.pyc │ ├── _rscp_encrypt_decrypt.cpython-38.pyc │ ├── _rscp_exceptions.cpython-37.pyc │ ├── _rscp_exceptions.cpython-38.pyc │ ├── _rscp_utils.cpython-37.pyc │ ├── _rscp_utils.cpython-38.pyc │ ├── e3dc.cpython-37.pyc │ ├── e3dc.cpython-38.pyc │ ├── rscp_helper.cpython-37.pyc │ ├── rscp_tag.cpython-37.pyc │ ├── rscp_type.cpython-37.pyc │ └── rscp_type.cpython-38.pyc ├── _rscp_dto.py ├── _rscp_encrypt_decrypt.py ├── _rscp_exceptions.py ├── _rscp_utils.py ├── e3dc.py ├── rscp_helper.py ├── rscp_tag.py └── rscp_type.py ├── e3dcwebgui.py ├── export.py ├── exportservice.py ├── gui.py ├── gui └── RSCP-GUI.fbp ├── images ├── RSCPGUI_Assistant.PNG ├── RSCPGUI_BAT.png ├── RSCPGUI_EMS.png ├── RSCPGUI_Ladeeinstellungen.png └── RSCPGUI_Wechselrichter.png ├── main.py ├── requirements.txt ├── rscpe3dc.conf.default.ini ├── rscpguiconsole.py ├── rscpguiframe.py └── rscpguimain.py /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 rxhan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RSCPGui 2 | Das Programm fragt Daten per RSCP von einem E3/DC - Hauskraftwerk ab und stellt diese dar. Die über RSCP veränderbaren Einstellungen im USER-Modus können auch geschrieben werden. Auch Aktionen wie z.B. Notstrombetrieb oder Systemneustart können ausgelöst werden. Dabei wird versucht möglichst viele Daten richtig darzustellen. Mit dem Programm können die Daten auch "headless" zyklisch exportiert werden. Unterstützt werden csv, json, influxdb, mqtt. Darüberhinaus ist es möglich Benachrichtigungen über Telegram zu erhalten, wenn bestimmte Ereignisse eintreten oder Werte gelesen wurden. Über MQTT können auch bestimmte Werte wieder zurückgeschrieben werden. 3 | 4 | **Das Programm befindet sich in einem frühen Entwicklungsstatus nicht alle Funktionen stehen zur Verfügung** 5 | **Benutzung erfolgt auf eigene Gefahr, für Schäden kann der Author nicht haftbar gemacht werden** 6 | 7 | Die Schnittstelle basiert auf der Library von MatrixCrawler (https://github.com/MatrixCrawler/python-e3dc-module). 8 | 9 | # Abhängigkeiten 10 | 11 | Getestet wurde mit Python 3.10/3.11, ältere Versionen bis 3.7 sollten ebenfalls funktionieren. 12 | Grundsätzlich sollte das Programm Plattformunabhängig sein, es wurde jedoch ausschließlich mit Windows getestet. 13 | 14 | 15 | # Installation 16 | 17 | ### Windows: 18 | 19 | Keine Installation notwendig, die Binary (RSCPGui.exe) kann direkt ausgeführt werden. 20 | 21 | Falls ohne Binary ausgeführt werden soll: 22 | Python3 muss inklusive der in requirements.txt beschriebenen Abhängigkeiten installiert sein. 23 | Aufruf dann mittels python3 main.py 24 | 25 | ### Linux: 26 | 27 | Voraussetzungen (für GUI-Betrieb) 28 | 29 | apt-get install git python3-dev libgtk-3-dev libpulse-dev python3-venv wheel 30 | pip3 install -r requirements.txt 31 | 32 | Für Konsolenbetrieb wenn z.B. nur Export der Daten genügt 33 | 34 | pip3 install -r requirements.txt 35 | 36 | Die Installation von wxPython wird dann fehlschlagen, dieses wird für den Konsolenbetrieb aber nicht benötigt. 37 | 38 | ### Konfiguration 39 | 40 | Beim ersten Start im Oberflächenmodus wird ein Assistent gestartet, über welchen eine Websocket-Verbindung mithilfe der Web-Zugangsdaten bequem hergestellt werden kann. Es ist jedoch zu empfehlen im Anschluss eine Direktverbindung einzurichten. Dazu muss dann im Einstellungs-Dialog das RSCP-Passwort hinterlegt werden, oder falls noch nicht geschehen gesetzt werden. 41 | 42 | ### manuelle Konfiguration 43 | 44 | Um die RSCPGui manuell zu konfigurieren muss lediglich die Datei rscpe3dc.conf.default.ini zu rscpe3dc.conf.ini umbenannt werden und diese mit den ensprechenden Werten gefüllt werden. 45 | 46 | Für eine korrekte Funktion der grundlegenden Programmeigenschaften sind mindestens folgende Einstellungen zu setzen: 47 | 48 | > [Login] 49 | > 50 | > username=mye3dportalusername 51 | > 52 | > password=mypassword 53 | > 54 | > rscppassword=myrscppassword 55 | > 56 | > address=192.168.xxx.yyy 57 | > 58 | > seriennummer=S10-xxxxxxxxxxxx 59 | 60 | # Nutzung 61 | 62 | Es werden mindestens Angaben zu Benutzername und Passwort benötigt. 63 | Weitere Informationen werden automatisch ermittelt sofern ein Internetzugriff besteht. 64 | Soll ein lokaler Zugriff erfolgen muss das RSCP-Passwort angegeben werden. 65 | Dies kann aber auch vom Programm gesetzt werden. 66 | 67 | ### Export von Daten 68 | 69 | Zum Export von Daten stehen CSV (historisch), JSON (Statusdatei), MQTT, Influx und ein URL-Post zur Verfügung. Die zu übertragenen Daten müssen vorher ausgewählt werden. 70 | Achtung: Es werden die RAW-Werte übertragen! Der Bezeichner entspricht dabei dem TOPIC in MQTT, oder der Überschrift der CSV-Datei bzw. der Key in der JSON-Datei. Es können auch ganze Knoten mit ihren Unterknoten umbenannt werden. Beim Export sollten nicht unnötig viele Daten angewählt werden. 71 | 72 | Um die Daten nun "headless" zu exportieren muss das Programm nach der Konfiguration mit **-e -c** gestartet werden. Dadurch wird (-e) der Export automatisch gestartet und das Programm startet im Konsolenmodus (-c). 73 | Die Konfigurationsdatei kann dafür an einem anderen Rechner mit Oberfläche erzeugt werden und auf den Zielrechner kopiert werden. (rscpgui.conf.ini) 74 | 75 | ### Benachrichtigungen 76 | 77 | Aktuell noch nicht ganz fertig! - Work in progess ;-) 78 | 79 | ### Werte über MQTT zurückschreiben 80 | 81 | Über MQTT können Werte auch wieder an das Hauskraftwerk zurückübertragen werden. Aktuell unterstützt werden: 82 | **maximale Ladeleistung, maximale Entladeleistung und untere Schwelle Lade-/Entladung.** 83 | Dies funktioniert nur bei aktiviertem Export. Zusätzlich müssen die zu ändernden Werte auch in den Exportdaten enthalten sein. Ein zurückschreiben der Werte ist nun mit dem angegebenen Bezeichner + /SET möglich. 84 | Ohne Anpassung des Bezeichners ist dies z.B. für die maximale Ladeleistung: 85 | *E3DC/EMS_DATA/EMS_GET_POWER_SETTINGS/EMS_MAX_CHARGE_POWER/SET* 86 | 87 | # Kommandozeilenparameter 88 | 89 | Können mit rscpgui.exe -h angezeigt werden: 90 | 91 | ```sh 92 | 93 | usage: rscpgui.exe [-h] [-e [EXPORT]] [-i] [-c [CONSOLE]] [-f [LOGFILE]] 94 | [-v [{CRITICAL,FATAL,ERROR,WARN,WARNING,INFO,DEBUG,NOTSET}]] [-l] [-p] 95 | ... 96 | ``` 97 | 98 | # Screenshots 99 | 100 | ![Assistent zur vereinfachten Anmeldung](https://github.com/rxhan/RSCPGui/blob/master/images/RSCPGUI_Assistant.PNG) 101 | 102 | ![Übersicht der Batteriedaten](https://github.com/rxhan/RSCPGui/blob/master/images/RSCPGUI_BAT.png) 103 | 104 | ![Übersicht der Ladeeinstellungen](https://github.com/rxhan/RSCPGui/blob/master/images/RSCPGUI_Ladeeinstellungen.png) 105 | 106 | ![Übersicht der Wechselrichter](https://github.com/rxhan/RSCPGui/blob/master/images/RSCPGUI_Wechselrichter.png) 107 | 108 | ![Übersicht der Basisdaten / EMS](https://github.com/rxhan/RSCPGui/blob/master/images/RSCPGUI_EMS.png) 109 | 110 | # Windows-Binary 111 | 112 | Aktuelle stehen in den Releasen bereit. 113 | 114 | https://github.com/rxhan/RSCPGui/releases 115 | 116 | # 117 | -------------------------------------------------------------------------------- /assistant.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from e3dc.rscp_tag import RSCPTag 4 | from e3dcwebgui import E3DCWebGui 5 | 6 | logger = logging.getLogger(__name__) 7 | 8 | logger.debug('Programmstart') 9 | 10 | from gui import AssistantFrame 11 | import wx 12 | 13 | class Assistant(AssistantFrame): 14 | 15 | def __init__(self, parent): 16 | self._parent = parent 17 | self._test_serial = None 18 | self._test_ip = None 19 | self._test_successfull = False 20 | self._testgui = None 21 | AssistantFrame.__init__(self, parent=parent) 22 | 23 | if self._parent.cfgLoginusername: 24 | self.txtAssistantUsername.SetValue(self._parent.cfgLoginusername) 25 | if self._parent.cfgLoginpassword: 26 | self.txtAssistantPassword.SetValue(self._parent.cfgLoginpassword) 27 | 28 | # Connect Events 29 | self.btnAssistantOK.Bind(wx.EVT_BUTTON, self.btnAssistantOKOnClick) 30 | self.btnAssistantCancel.Bind(wx.EVT_BUTTON, self.btnAssistantCancelOnClick) 31 | 32 | def btnAssistantOKOnClick( self, event ): 33 | pwd = self.txtAssistantPassword.GetValue() 34 | user = self.txtAssistantUsername.GetValue() 35 | 36 | if not pwd or not user: 37 | logger.warning('Passwort oder Benutzer fehlen, kein Login möglich') 38 | else: 39 | ret = self.tryWeblogin(user, pwd) 40 | if ret: 41 | logger.info('Assistent bestätigt, Zugangsdaten erfolgreich') 42 | self.Close() 43 | else: 44 | logger.info('Zugangsdaten falsch, Assistent offen halten') 45 | 46 | def btnAssistantCancelOnClick( self, event ): 47 | logger.info('Assistent beendet') 48 | 49 | self.Close() 50 | 51 | @property 52 | def username(self): 53 | return self.txtAssistantUsername.GetValue() 54 | 55 | @property 56 | def password(self): 57 | return self.txtAssistantPassword.GetValue() 58 | 59 | @property 60 | def no_show(self): 61 | return self.chkAsistantNoShow.GetValue() 62 | 63 | @property 64 | def serial(self): 65 | return self._test_serial 66 | 67 | @property 68 | def ip(self): 69 | return self._test_ip 70 | 71 | @property 72 | def testresult(self): 73 | return self._test_successfull 74 | 75 | @property 76 | def testgui(self): 77 | return self._testgui 78 | 79 | def tryWeblogin(self, username, password): 80 | def test_connection(testgui): 81 | requests = [] 82 | requests.append(RSCPTag.INFO_REQ_SERIAL_NUMBER) 83 | requests.append(RSCPTag.INFO_REQ_IP_ADDRESS) 84 | return testgui.get_data(requests, True) 85 | 86 | try: 87 | ret = self._parent.getSerialnoFromWeb(username, password) 88 | if len(ret) == 1: 89 | seriennummer = [self._parent.getSNFromNumbers(ret[0]['serialno'])] 90 | logger.debug(f'Seriennummer konnte ermittelt werden (WEB): {seriennummer}') 91 | logger.debug('Versuche IP-Adresse zu ermitteln') 92 | try: 93 | self._testgui = E3DCWebGui(username, password, seriennummer[0]) 94 | ip = repr(test_connection(self._testgui)['INFO_IP_ADDRESS']) 95 | if ip: 96 | logger.debug('IP-Adresse konnte ermittelt werden: ' + ip) 97 | else: 98 | raise Exception('IP-Adresse konnte nicht ermittelt werden, kein Inhalt') 99 | except: 100 | logger.exception('Bei der Ermittlung der IP-Adresse ist ein Fehler aufgetreten') 101 | self._testgui = None 102 | wx.MessageBox(f'Websocket-Verbindung zu E3DC nicht möglich.\nFirewalleinstellungen prüfen oder es später erneut versuchen.\nErmittelte Seriennummer: {seriennummer[0]}', 'Verbindungsfehler', wx.ICON_ERROR) 103 | else: 104 | self._test_serial = seriennummer[0] 105 | self._test_ip = ip 106 | self._test_successfull = True 107 | #wx.MessageBox(f'Zugriff erfolgreich, ermitteltes System:\nSeriennummer: {seriennummer[0]}\nIP: {ip}', 'Zugriff hergestellt', wx.ICON_INFORMATION) 108 | return True 109 | 110 | elif len(ret) > 1: 111 | seriennummer = [self._parent.getSNFromNumbers(sn['serialno']) for sn in ret] 112 | logger.debug('Es wurde mehr als eine Seriennummer ermittelt (WEB):' + '\n'.join(seriennummer)) 113 | wx.MessageBox(f'Es wurden mehrere Seriennummer zu dem Webzugang ermittelt.\nBitte Einstellungen benutzen.', 'Mehrere Seriennummern', wx.ICON_INFORMATION) 114 | else: 115 | wx.MessageBox('Verbindung nicht möglich, Zugangsdaten falsch?', 'Zugangsdaten falsch?', wx.ICON_ERROR) 116 | except: 117 | logger.exception('Ermittlung von IP und Seriennummer nicht möglich. Zugangsdaten falsch?') 118 | wx.MessageBox('Verbindung nicht möglich, Zugangsdaten falsch?', 'Zugangsdaten falsch?', wx.ICON_ERROR) 119 | 120 | 121 | return False 122 | 123 | -------------------------------------------------------------------------------- /e3dc/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rxhan/RSCPGui/90a7315be0c5c9f3f14eb37822eb1dd781050c09/e3dc/__init__.py -------------------------------------------------------------------------------- /e3dc/__pycache__/__init__.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rxhan/RSCPGui/90a7315be0c5c9f3f14eb37822eb1dd781050c09/e3dc/__pycache__/__init__.cpython-37.pyc -------------------------------------------------------------------------------- /e3dc/__pycache__/__init__.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rxhan/RSCPGui/90a7315be0c5c9f3f14eb37822eb1dd781050c09/e3dc/__pycache__/__init__.cpython-38.pyc -------------------------------------------------------------------------------- /e3dc/__pycache__/_rscp_dto.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rxhan/RSCPGui/90a7315be0c5c9f3f14eb37822eb1dd781050c09/e3dc/__pycache__/_rscp_dto.cpython-37.pyc -------------------------------------------------------------------------------- /e3dc/__pycache__/_rscp_dto.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rxhan/RSCPGui/90a7315be0c5c9f3f14eb37822eb1dd781050c09/e3dc/__pycache__/_rscp_dto.cpython-38.pyc -------------------------------------------------------------------------------- /e3dc/__pycache__/_rscp_encrypt_decrypt.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rxhan/RSCPGui/90a7315be0c5c9f3f14eb37822eb1dd781050c09/e3dc/__pycache__/_rscp_encrypt_decrypt.cpython-37.pyc -------------------------------------------------------------------------------- /e3dc/__pycache__/_rscp_encrypt_decrypt.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rxhan/RSCPGui/90a7315be0c5c9f3f14eb37822eb1dd781050c09/e3dc/__pycache__/_rscp_encrypt_decrypt.cpython-38.pyc -------------------------------------------------------------------------------- /e3dc/__pycache__/_rscp_exceptions.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rxhan/RSCPGui/90a7315be0c5c9f3f14eb37822eb1dd781050c09/e3dc/__pycache__/_rscp_exceptions.cpython-37.pyc -------------------------------------------------------------------------------- /e3dc/__pycache__/_rscp_exceptions.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rxhan/RSCPGui/90a7315be0c5c9f3f14eb37822eb1dd781050c09/e3dc/__pycache__/_rscp_exceptions.cpython-38.pyc -------------------------------------------------------------------------------- /e3dc/__pycache__/_rscp_utils.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rxhan/RSCPGui/90a7315be0c5c9f3f14eb37822eb1dd781050c09/e3dc/__pycache__/_rscp_utils.cpython-37.pyc -------------------------------------------------------------------------------- /e3dc/__pycache__/_rscp_utils.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rxhan/RSCPGui/90a7315be0c5c9f3f14eb37822eb1dd781050c09/e3dc/__pycache__/_rscp_utils.cpython-38.pyc -------------------------------------------------------------------------------- /e3dc/__pycache__/e3dc.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rxhan/RSCPGui/90a7315be0c5c9f3f14eb37822eb1dd781050c09/e3dc/__pycache__/e3dc.cpython-37.pyc -------------------------------------------------------------------------------- /e3dc/__pycache__/e3dc.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rxhan/RSCPGui/90a7315be0c5c9f3f14eb37822eb1dd781050c09/e3dc/__pycache__/e3dc.cpython-38.pyc -------------------------------------------------------------------------------- /e3dc/__pycache__/rscp_helper.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rxhan/RSCPGui/90a7315be0c5c9f3f14eb37822eb1dd781050c09/e3dc/__pycache__/rscp_helper.cpython-37.pyc -------------------------------------------------------------------------------- /e3dc/__pycache__/rscp_tag.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rxhan/RSCPGui/90a7315be0c5c9f3f14eb37822eb1dd781050c09/e3dc/__pycache__/rscp_tag.cpython-37.pyc -------------------------------------------------------------------------------- /e3dc/__pycache__/rscp_type.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rxhan/RSCPGui/90a7315be0c5c9f3f14eb37822eb1dd781050c09/e3dc/__pycache__/rscp_type.cpython-37.pyc -------------------------------------------------------------------------------- /e3dc/__pycache__/rscp_type.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rxhan/RSCPGui/90a7315be0c5c9f3f14eb37822eb1dd781050c09/e3dc/__pycache__/rscp_type.cpython-38.pyc -------------------------------------------------------------------------------- /e3dc/_rscp_dto.py: -------------------------------------------------------------------------------- 1 | import binascii 2 | import copy 3 | import datetime 4 | import traceback 5 | from typing import Optional, Union 6 | 7 | from e3dc.rscp_tag import RSCPTag, RSCPTag2Type 8 | from e3dc.rscp_type import RSCPType, ERROR_CODE 9 | 10 | """ 11 | This is a data wrapper to send and receive data to the e3dc. 12 | It consists of a tag, the type, the data and the size of the data. 13 | """ 14 | 15 | 16 | class RSCPDTO: 17 | def __init__(self, tag: RSCPTag, rscp_type: RSCPType = RSCPType.Nil, 18 | data: Union[list, float, str, None, bytes] = None, 19 | size: Optional[int] = None, rscpdata: bytes = None): 20 | self.tag = tag 21 | self.type = rscp_type 22 | self.rscpdata = rscpdata 23 | 24 | if rscp_type == RSCPType.Nil: 25 | res = RSCPTag2Type.__getattr__(tag.name) 26 | if res: 27 | self.type = res['type'] 28 | else: 29 | if isinstance(data, list): 30 | iscontainer = True 31 | if len(data) > 0: 32 | for l in data: 33 | if not isinstance(l, RSCPTag) and not isinstance(l, RSCPDTO): 34 | iscontainer = False 35 | 36 | if iscontainer: 37 | print('iscontainer', self.tag.name) 38 | self.type = RSCPType.Container 39 | 40 | self.data = data 41 | 42 | if self.type == RSCPType.Container and not data: 43 | self.data = [] 44 | else: 45 | self.data = data 46 | self.size = size 47 | self.current_pos = 0 48 | 49 | @property 50 | def rscpdata_str(self): 51 | if self.rscpdata: 52 | return str(binascii.hexlify(self.rscpdata)) 53 | else: 54 | return 'None' 55 | 56 | def __add__(self, other): 57 | if self.type == RSCPType.Container: 58 | if isinstance(self.data, list): 59 | basis = self 60 | if isinstance(other, RSCPDTO): 61 | basis.data.append(other) 62 | return basis 63 | elif isinstance(other, RSCPTag): 64 | basis.data.append(RSCPDTO(tag=other)) 65 | return basis 66 | elif isinstance(other, list): 67 | basis.data += other 68 | return basis 69 | 70 | raise ArithmeticError 71 | 72 | def __copy__(self): 73 | return RSCPDTO(tag=self.tag, rscp_type=self.type, data=copy.copy(self.data), size=self.size) 74 | 75 | def __iter__(self): 76 | return self 77 | 78 | def __next__(self): 79 | if isinstance(self.data, list): 80 | while self.current_pos < len(self.data): 81 | ret = self.data[self.current_pos] 82 | self.current_pos += 1 83 | return ret 84 | elif self.current_pos == 0: 85 | self.current_pos += 1 86 | return self.data 87 | 88 | self.current_pos = 0 89 | raise StopIteration() 90 | 91 | def __getitem__(self, key): 92 | if isinstance(self.data, list): 93 | result = [] 94 | for data in self.data: 95 | if data.name == key: 96 | result.append(data) 97 | 98 | if len(result) == 1: 99 | data = result[0] 100 | if isinstance(data.data, list): 101 | return data 102 | else: 103 | return data 104 | elif len(result) > 1: 105 | return result 106 | else: 107 | if isinstance(self.data, RSCPDTO): 108 | if self.data.tag == RSCPTag.LIST_TYPE: 109 | return self.data[key] 110 | elif self.data.name == key: 111 | return self.data 112 | 113 | raise AttributeError() 114 | 115 | def __cmp__(self, item): 116 | if isinstance(item, RSCPDTO): 117 | if self.name == item.name: 118 | return True 119 | elif isinstance(item, RSCPTag): 120 | if self.tag == item: 121 | return True 122 | 123 | return False 124 | 125 | def __len__(self): 126 | if isinstance(self.data, list): 127 | return len(self.data) 128 | else: 129 | return 1 130 | 131 | def __contains__(self, item): 132 | if isinstance(self.data, list): 133 | for data in self.data: 134 | if isinstance(item, str): 135 | if data.name == item: 136 | return True 137 | elif isinstance(item, RSCPTag): 138 | if data.name == item.name: 139 | return True 140 | elif isinstance(item, RSCPDTO): 141 | if data == item: 142 | return True 143 | else: 144 | if isinstance(self.data, RSCPDTO): 145 | if self.data.tag == RSCPTag.LIST_TYPE: 146 | return item in self.data 147 | elif self.data.name == item: 148 | return True 149 | else: 150 | return False 151 | 152 | return False 153 | 154 | def set_data(self, value): 155 | if self.type == RSCPType.Container and isinstance(value, list): 156 | for k, l in enumerate(value): 157 | if isinstance(l, RSCPTag): 158 | value[k] = RSCPDTO(l) 159 | elif self.type == RSCPType.Nil: 160 | if value is not None: 161 | raise AttributeError('Daten bei leerem Datentyp nicht erlaubt') 162 | 163 | self._data = value 164 | 165 | def get_data(self): 166 | return self._data 167 | 168 | def countItems(self, name): 169 | i = 0 170 | if isinstance(self.data, list): 171 | for e in self.data: 172 | if isinstance(e, RSCPDTO): 173 | if e.name == name: 174 | i += 1 175 | 176 | return i 177 | 178 | def getItemsByName(self, name): 179 | if isinstance(self.data, list): 180 | ret = [] 181 | for e in self.data: 182 | if isinstance(e, RSCPDTO): 183 | if e.name == name: 184 | ret.append(e) 185 | 186 | return ret 187 | elif isinstance(self.data, RSCPDTO): 188 | if self.data.name == name: 189 | return [self.data] 190 | return None 191 | 192 | def asDict(self, translate=False): 193 | if self.type == RSCPType.Container: 194 | obj = {} 195 | dat: RSCPDTO 196 | for dat in self.data: 197 | if isinstance(dat, RSCPDTO): 198 | name = dat.name 199 | cnt = self.countItems(name) 200 | if cnt > 1: 201 | if name not in obj: 202 | obj[name] = [] 203 | tmp = dat.asDict(translate) 204 | obj[name].append(tmp[name]) 205 | else: 206 | d: dict = dat.asDict(translate) 207 | if isinstance(obj, list): 208 | obj = obj + [d] 209 | elif len([k for k in d.keys() if k in obj.keys()]) > 0: 210 | obj = [obj, d] 211 | else: 212 | obj = {**obj, **d} 213 | else: 214 | if not translate: 215 | obj = self.data 216 | else: 217 | obj = repr(self) 218 | 219 | if self.tag == RSCPTag.LIST_TYPE: 220 | return obj 221 | else: 222 | return {self.name: obj} 223 | 224 | def __round__(self, n=None): 225 | if self.type != RSCPType.Error: 226 | return round(self.data, n) 227 | else: 228 | return 0.0 229 | 230 | def __int__(self): 231 | if self.type != RSCPType.Error: 232 | return int(self.data) 233 | else: 234 | return 0 235 | 236 | def __float__(self): 237 | if self.type != RSCPType.Error: 238 | return float(self.data) 239 | else: 240 | return 0.0 241 | 242 | def __str__(self): 243 | messages = [] 244 | if self.type == RSCPType.Container: 245 | messages.append( 246 | "rscp: \t tag: " + self.tag.name + "\t type: " + self.type.name + " \t rscpdata: " + self.rscpdata_str) 247 | for dat in self.data: 248 | ret = str(dat) 249 | ret = ret.replace("\n", "\n\t") 250 | messages.append(" |--> " + ret) 251 | else: 252 | try: 253 | attr = RSCPTag2Type.__getattr__(self.tag.name) 254 | if attr: 255 | if not isinstance(attr['type'], RSCPType): 256 | enum_value = attr['type'](self.data).name 257 | messages.append( 258 | "rscp: \t tag: " + self.tag.name + "\t type: " + self.type.name + "\t data: " + str( 259 | self.data) + "\t enumtype: " + str(attr['type']) + "\t enumdata: " + str( 260 | enum_value) + " \t rscpdata: " + self.rscpdata_str) 261 | return "\n".join(messages) 262 | except: 263 | traceback.print_exc() 264 | pass 265 | if self.type == RSCPType.ByteArray: 266 | data = binascii.hexlify(self.data) 267 | messages.append("rscp: \t tag: " + self.tag.name + "\t type: " + self.type.name + "\t data: " + str( 268 | data) + " \t rscpdata: " + self.rscpdata_str) 269 | 270 | elif self.type == RSCPType.Error: 271 | try: 272 | error_code = ERROR_CODE(self.data) 273 | except ValueError: 274 | error_code = str(self.data) + ' (UNKNOWN)' 275 | messages.append("rscp: \t tag: " + self.tag.name + "\t type: " + self.type.name + "\t data: " + 276 | str(error_code) + " \t rscpdata: " + self.rscpdata_str) 277 | elif self.type == RSCPType.Timestamp: 278 | messages.append("rscp: \t tag: " + self.tag.name + "\t type: " + self.type.name + "\t data: " + 279 | datetime.datetime.utcfromtimestamp(self.data).isoformat() + " (Dt: " + 280 | str(self.data) + ")" + " \t rscpdata: " + self.rscpdata_str) 281 | else: 282 | messages.append("rscp: \t tag: " + self.tag.name + "\t type: " + self.type.name + "\t data: " + str( 283 | self.data) + " \t rscpdata: " + self.rscpdata_str) 284 | 285 | return "\n".join(messages) 286 | 287 | def __repr__(self): 288 | if self.type == RSCPType.Container: 289 | return None 290 | else: 291 | try: 292 | attr = RSCPTag2Type.__getattr__(self.tag.name) 293 | if attr: 294 | if not isinstance(attr['type'], RSCPType): 295 | enum_value = attr['type'](self.data).name 296 | return str(enum_value) 297 | except: 298 | traceback.print_exc() 299 | 300 | return str(self.data) 301 | 302 | def get_name(self): 303 | return self.tag.name 304 | 305 | name = property(get_name) 306 | data = property(get_data, set_data) 307 | -------------------------------------------------------------------------------- /e3dc/_rscp_encrypt_decrypt.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import math 3 | from typing import Union 4 | 5 | from py3rijndael import RijndaelCbc, ZeroPadding 6 | 7 | logger = logging.getLogger(__name__) 8 | 9 | 10 | class ParameterError(Exception): 11 | def __init__(self, message): 12 | logger.exception(message) 13 | 14 | 15 | class RSCPEncryptDecrypt: 16 | KEY_SIZE: int = 32 17 | BLOCK_SIZE: int = 32 18 | 19 | def __init__(self, key: str): 20 | if len(key) > self.KEY_SIZE: 21 | raise ParameterError("Key must be <%d bytes" % self.KEY_SIZE) 22 | 23 | self.key = bytes(key.ljust(self.KEY_SIZE, '\xff'), encoding="latin_1") 24 | self.encrypt_init_vector = bytes('\xff' * self.BLOCK_SIZE, encoding="latin_1") 25 | self.decrypt_init_vector = bytes('\xff' * self.BLOCK_SIZE, encoding="latin_1") 26 | self.remaining_data = '' 27 | self.old_decrypt = '' 28 | 29 | def encrypt(self, plain_data: Union[str, bytes]) -> bytes: 30 | if isinstance(plain_data, str): 31 | plain_data = bytes(plain_data, encoding="latin_1") 32 | cbc = RijndaelCbc(key=self.key, iv=self.encrypt_init_vector, padding=ZeroPadding(self.BLOCK_SIZE), 33 | block_size=self.BLOCK_SIZE) 34 | encrypted_data = cbc.encrypt(plain_data) 35 | self.encrypt_init_vector = encrypted_data[-self.BLOCK_SIZE:] 36 | return encrypted_data 37 | 38 | def decrypt(self, encrypted_data, previously_processed_data_index=None) -> bytes: 39 | if previously_processed_data_index is None: 40 | length = len(self.old_decrypt) 41 | if length % self.BLOCK_SIZE == 0: 42 | previously_processed_data_index = length 43 | else: 44 | previously_processed_data_index = int(self.BLOCK_SIZE * math.floor(length / self.BLOCK_SIZE)) 45 | if previously_processed_data_index % self.BLOCK_SIZE != 0: 46 | previously_processed_data_index = int( 47 | self.BLOCK_SIZE * math.ceil(previously_processed_data_index / self.BLOCK_SIZE)) 48 | remaining_data = self.old_decrypt[previously_processed_data_index:] 49 | if self.old_decrypt != '': 50 | self.decrypt_init_vector = self.old_decrypt[ 51 | previously_processed_data_index - self.BLOCK_SIZE:previously_processed_data_index] 52 | self.old_decrypt = encrypted_data 53 | 54 | cbc = RijndaelCbc(key=self.key, iv=self.decrypt_init_vector, padding=ZeroPadding(self.BLOCK_SIZE), 55 | block_size=self.BLOCK_SIZE) 56 | decrypt = cbc.decrypt(encrypted_data) 57 | return decrypt 58 | -------------------------------------------------------------------------------- /e3dc/_rscp_exceptions.py: -------------------------------------------------------------------------------- 1 | from logging import Logger 2 | 3 | 4 | class RSCPFrameError(Exception): 5 | def __init__(self, message: str, logger: Logger): 6 | if message is None: 7 | message = self.__class__.__name__ 8 | logger.exception(message) 9 | 10 | 11 | class RSCPDataError(Exception): 12 | def __init__(self, message: str, logger: Logger): 13 | if message is None: 14 | message = self.__class__.__name__ 15 | logger.exception(message) 16 | 17 | 18 | class RSCPAuthenticationError(Exception): 19 | def __init__(self, message: str, logger: Logger): 20 | if message is None: 21 | message = self.__class__.__name__ 22 | logger.exception(message) 23 | 24 | 25 | class RSCPCommunicationError(Exception): 26 | def __init__(self, message, logger: Logger, response=None): 27 | if message is None: 28 | message = self.__class__.__name__ 29 | self.response = response 30 | self.message = message 31 | logger.debug(message) 32 | -------------------------------------------------------------------------------- /e3dc/_rscp_utils.py: -------------------------------------------------------------------------------- 1 | import binascii 2 | import datetime 3 | import logging 4 | import math 5 | import struct 6 | import time 7 | import traceback 8 | import zlib 9 | 10 | from e3dc._rscp_dto import RSCPDTO 11 | from e3dc._rscp_exceptions import RSCPFrameError, RSCPDataError 12 | from e3dc.rscp_tag import RSCPTag 13 | from e3dc.rscp_type import RSCPType 14 | 15 | logger = logging.getLogger(__name__) 16 | 17 | # TODO: Performance: In Statische Methoden umwandeln 18 | class RSCPUtils: 19 | _FRAME_HEADER_FORMAT = " bytes: 26 | magic_byte = self._endian_swap_uint16(0xe3dc) 27 | if crc: 28 | ctrl_byte = self._endian_swap_uint16(0x11) 29 | else: 30 | ctrl_byte = self._endian_swap_uint16(0x01) 31 | current_time = time.time() 32 | seconds = math.ceil(current_time) 33 | nanoseconds = round((current_time - int(current_time)) * 1000) 34 | length = len(data) 35 | frame = struct.pack(self._FRAME_HEADER_FORMAT + str(length) + "s", magic_byte, ctrl_byte, seconds, nanoseconds, 36 | length, data) 37 | if crc: 38 | checksum = zlib.crc32(frame) % (1 << 32) 39 | frame += struct.pack(self._FRAME_CRC_FORMAT, checksum) 40 | return frame 41 | 42 | def encode_data(self, rscp_dto: RSCPDTO) -> bytes: 43 | pack_format = self._DATA_HEADER_FORMAT 44 | data_header_length = struct.calcsize(self._DATA_HEADER_FORMAT) 45 | if rscp_dto.type == RSCPType.Nil: 46 | return struct.pack(self._DATA_HEADER_FORMAT, rscp_dto.tag.value, rscp_dto.type.value, 0) 47 | elif rscp_dto.type == RSCPType.Timestamp: 48 | timestamp = int(rscp_dto.data) 49 | milliseconds = int((rscp_dto.data - timestamp) * 1e9) 50 | high = timestamp >> 32 51 | low = timestamp & 0xffffffff 52 | length = struct.calcsize("iii") - data_header_length 53 | return struct.pack(self._DATA_HEADER_FORMAT + "iii", rscp_dto.tag.value, rscp_dto.type.value, length, high, low, milliseconds) 54 | elif rscp_dto.type == RSCPType.Container: 55 | if isinstance(rscp_dto.data, list): 56 | new_data = b'' 57 | for data_chunk in rscp_dto.data: 58 | new_data += self.encode_data(data_chunk) 59 | rscp_dto.data = new_data 60 | pack_format += str(len(rscp_dto.data)) + rscp_dto.type.mapping 61 | elif rscp_dto.type.mapping in ("s","r"): 62 | if isinstance(rscp_dto.data, str): 63 | # We do expect a string object. Make it to bytes array 64 | rscp_dto.data = bytes(rscp_dto.data, encoding="latin_1") 65 | pack_format += str(len(rscp_dto.data)) + "s" 66 | elif rscp_dto.type.mapping != "s": 67 | pack_format += rscp_dto.type.mapping 68 | 69 | data_length = struct.calcsize(pack_format) - data_header_length 70 | logger.debug("pack_format: " + pack_format) 71 | logger.debug("data: " + str(rscp_dto.data)) 72 | try: 73 | res = struct.pack(pack_format, rscp_dto.tag.value, rscp_dto.type.value, data_length, rscp_dto.data) 74 | except: 75 | traceback.print_exc() 76 | 77 | return res 78 | 79 | def _decode_frame(self, frame_data) -> tuple: 80 | """ 81 | 82 | :param frame_data: 83 | :return: 84 | """ 85 | crc = None 86 | magic, ctrl, seconds, nanoseconds, length = struct.unpack(self._FRAME_HEADER_FORMAT, frame_data[ 87 | :struct.calcsize( 88 | self._FRAME_HEADER_FORMAT)]) 89 | 90 | logger.debug(f'Decoded Frame: Magic: {hex(magic)} Ctrl: {ctrl} Timestamp: {datetime.datetime.fromtimestamp(seconds).isoformat()} Nanoseconds: {nanoseconds} length: {length} count: {len(frame_data)}') 91 | 92 | if ctrl & 0x10: 93 | total_length = struct.calcsize(self._FRAME_HEADER_FORMAT) + length + struct.calcsize(self._FRAME_CRC_FORMAT) 94 | data, crc = struct.unpack("<" + str(length) + "s" + self._FRAME_CRC_FORMAT, 95 | frame_data[struct.calcsize(self._FRAME_HEADER_FORMAT):total_length]) 96 | logger.debug(f"CRC is enabled, payload-size: {len(data)} Bytes, crc: {crc}") 97 | else: 98 | total_length = struct.calcsize(self._FRAME_HEADER_FORMAT) + length 99 | data = \ 100 | struct.unpack("<" + str(length) + "s", 101 | frame_data[struct.calcsize(self._FRAME_HEADER_FORMAT):total_length])[ 102 | 0] 103 | logger.debug(f"CRC is disabled, payload-size: {len(data)} Bytes") 104 | 105 | self._check_crc_validity(crc, frame_data) 106 | timestamp = seconds + float(nanoseconds) / 1000 107 | return data, timestamp 108 | 109 | def decode_server_data(self, data) -> RSCPDTO: 110 | if isinstance(data, str): 111 | data = binascii.unhexlify(data) 112 | 113 | rscp_dto = self.decode_data(data) 114 | if 'SERVER_RSCP_DATA' in rscp_dto: 115 | f = rscp_dto['SERVER_RSCP_DATA'] 116 | if f: 117 | b = f.data 118 | res, timestamp = self._decode_frame(b) 119 | f.type = RSCPType.Container 120 | data = self.decode_data(res) 121 | f.data = data 122 | 123 | return rscp_dto 124 | 125 | def decode_data(self, data: bytes, offline=False) -> RSCPDTO: 126 | magic_byte = struct.unpack(self._MAGIC_CHECK_FORMAT, data[:struct.calcsize(self._MAGIC_CHECK_FORMAT)])[0] 127 | if magic_byte == 0xe3dc: 128 | decode_frame_result = self._decode_frame(data) 129 | return self.decode_data(decode_frame_result[0]) 130 | 131 | data_header_size = struct.calcsize(self._DATA_HEADER_FORMAT) 132 | data_tag_hex, data_type_hex, data_length = struct.unpack(self._DATA_HEADER_FORMAT, 133 | data[:data_header_size]) 134 | 135 | logger.debug(f'Decode_Data - Header Size: {data_header_size} tag_hex: {hex(data_tag_hex)} type_hex: {hex(data_type_hex)} length: {data_length}') 136 | try: 137 | data_tag = RSCPTag(data_tag_hex) 138 | except: 139 | logger.error(f'Unbekannter RSCPTag: {data_tag_hex}') 140 | data_tag = RSCPTag.UKNOWN 141 | 142 | data_type = RSCPType(data_type_hex) 143 | 144 | logger.debug(f'Decoded_HeaderData - data_tag: {data_tag} data_type{data_type}') 145 | 146 | # Für Datensammlungen ohne Container, es wird ein Dummy gebildet 147 | if data_header_size + data_length != len(data): 148 | container_data = [] 149 | current_byte = 0 150 | while current_byte < len(data): 151 | data_tag_hex_c, data_type_hex_c, data_length_c = struct.unpack(self._DATA_HEADER_FORMAT, 152 | data[ 153 | current_byte:current_byte + data_header_size]) 154 | inner_rscp_dto = self.decode_data(data[current_byte:data_header_size + current_byte + data_length_c]) 155 | current_byte += inner_rscp_dto.size 156 | container_data.append(inner_rscp_dto) 157 | return RSCPDTO(RSCPTag.LIST_TYPE, RSCPType.Container, container_data, current_byte, rscpdata=data) 158 | # Check the data type name to handle the values accordingly 159 | elif data_type == RSCPType.Container: 160 | container_data = [] 161 | current_byte = data_header_size 162 | while current_byte < data_header_size + data_length: 163 | # inner_data, used_length = self.decode_data(data[current_byte:]) 164 | data_tag_hex_c, data_type_hex_c, data_length_c = struct.unpack(self._DATA_HEADER_FORMAT, 165 | data[current_byte:current_byte + data_header_size]) 166 | inner_rscp_dto = self.decode_data(data[current_byte:data_header_size + current_byte + data_length_c]) 167 | current_byte += inner_rscp_dto.size 168 | container_data.append(inner_rscp_dto) 169 | return RSCPDTO(data_tag, data_type, container_data, current_byte, rscpdata=data) 170 | elif data_type == RSCPType.Timestamp: 171 | data_format = " tuple: 212 | return struct.unpack("H", val))[0] 213 | -------------------------------------------------------------------------------- /e3dc/e3dc.py: -------------------------------------------------------------------------------- 1 | import binascii 2 | import logging 3 | import platform 4 | import socket 5 | import time 6 | from typing import Union 7 | 8 | from e3dc._rscp_dto import RSCPDTO 9 | from e3dc._rscp_encrypt_decrypt import RSCPEncryptDecrypt 10 | from e3dc._rscp_exceptions import RSCPAuthenticationError, RSCPCommunicationError 11 | from e3dc.rscp_tag import RSCPTag 12 | from e3dc.rscp_type import RSCPType 13 | from e3dc._rscp_utils import RSCPUtils 14 | 15 | logger = logging.getLogger(__name__) 16 | 17 | class E3DC: 18 | PORT = 5033 19 | BUFFER_SIZE = 1024*32 20 | 21 | def __init__(self, username, password, ip, key): 22 | self.password = password 23 | self.username = username 24 | self.ip = ip 25 | self.socket = None 26 | self.key = key 27 | self.waittime = 0.01 28 | self.rscp_utils = RSCPUtils() 29 | 30 | def create_encrypt(self): 31 | self.encrypt_decrypt = RSCPEncryptDecrypt(self.key) 32 | 33 | def send_requests2(self, payload: [Union[RSCPDTO, RSCPTag]], waittime = 0.0) -> [RSCPDTO]: 34 | """ 35 | This function will send a list of requests consisting of RSCPDTO's oder RSCPTag's to the e3dc 36 | and returns a list of responses. 37 | 38 | i.e. responses = send_requests([RSCPTag.EMS_REQ_BAT_SOC, RSCPTag.EMS_REQ_POWER_PV, 39 | RSCPTag.EMS_REQ_POWER_BAT, RSCPTag.EMS_REQ_POWER_GRID, 40 | RSCPTag.EMS_REQ_POWER_WB_ALL]) 41 | :param payload: A list of requests 42 | :return: A list of responses in form of RSCPDTO's 43 | """ 44 | dto_list: [RSCPDTO] = [] 45 | for payload_element in payload: 46 | if isinstance(payload_element, RSCPTag): 47 | dto_list.append(RSCPDTO(payload_element)) 48 | else: 49 | dto_list.append(payload_element) 50 | logger.debug("Sending " + str(len(dto_list)) + " requests to " + str(self.ip)) 51 | responses: [RSCPDTO] = [] 52 | dto: RSCPDTO 53 | for dto in dto_list: 54 | response = self.send_request(dto, True, waittime=waittime) 55 | responses.append(response) 56 | return responses 57 | 58 | def send_requests(self, payload: [Union[RSCPDTO, RSCPTag]], waittime = 0.0) -> [RSCPDTO]: 59 | payload_all = bytes() 60 | for payload_element in payload: 61 | if isinstance(payload_element, RSCPTag): 62 | dto = RSCPDTO(payload_element) 63 | else: 64 | dto = payload_element 65 | 66 | payload_all+=self.rscp_utils.encode_data(dto) 67 | 68 | prepared_data = self.rscp_utils.encode_frame(payload_all) 69 | response = self.send_request(prepared_data, True, waittime) 70 | 71 | responses: [RSCPDTO] = [] 72 | if response.type == RSCPType.Container: 73 | data: RSCPDTO 74 | for data in response: 75 | responses.append(data) 76 | else: 77 | responses.append(response) 78 | 79 | return responses 80 | 81 | def send_request(self, payload: Union[RSCPDTO, RSCPTag, bytes], keep_connection_alive: bool = False, waittime: float = 0.0) -> RSCPDTO: 82 | """ 83 | This will perform a single request. 84 | 85 | :param payload: The payload that defines the request 86 | :param keep_connection_alive: A flag whether to keep the connection alive or not 87 | :return: A response object as RSCPDTO 88 | """ 89 | if isinstance(payload, RSCPTag): 90 | payload = RSCPDTO(payload) 91 | if self.socket is None: 92 | self._connect() 93 | 94 | if isinstance(payload, bytes): 95 | prepared_data = payload 96 | else: 97 | encode_data = self.rscp_utils.encode_data(payload) 98 | prepared_data = self.rscp_utils.encode_frame(encode_data) 99 | 100 | #rawdata = binascii.hexlify(prepared_data) 101 | #logger.debug('Send RAW: ' + str(rawdata)) 102 | logger.debug('Send ' + str(len(prepared_data)) + ' Bytes') 103 | encrypted_data = self.encrypt_decrypt.encrypt(prepared_data) 104 | try: 105 | self.socket.send(encrypted_data) 106 | except: 107 | self._disconnect() 108 | raise 109 | 110 | wait = self.waittime + waittime 111 | if wait > 0.0: 112 | time.sleep(wait) 113 | 114 | response = self._receive() 115 | if response.type == RSCPType.Error: 116 | logger.debug("Error type returned: " + str(response.data)) 117 | raise (RSCPCommunicationError('Error type returned: ' + str(response.data), logger, response)) 118 | if not keep_connection_alive: 119 | self._disconnect() 120 | return response 121 | 122 | def _connect(self): 123 | if self.socket is None: 124 | logger.info("Trying to establish connection to " + str(self.ip)) 125 | self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 126 | self.socket.connect((self.ip, self.PORT)) 127 | self.socket.setblocking(False) 128 | rscp_dto = RSCPDTO(RSCPTag.RSCP_REQ_AUTHENTICATION, RSCPType.Container, 129 | [RSCPDTO(RSCPTag.RSCP_AUTHENTICATION_USER, RSCPType.CString, self.username), 130 | RSCPDTO(RSCPTag.RSCP_AUTHENTICATION_PASSWORD, RSCPType.CString, self.password)], None) 131 | self.create_encrypt() 132 | result = self.send_request(rscp_dto, True) 133 | if result.type == RSCPType.Error: 134 | self._disconnect() 135 | raise RSCPAuthenticationError("Invalid username or password", logger) 136 | 137 | def _disconnect(self): 138 | logger.info("Closing connection to " + str(self.ip)) 139 | self.socket.close() 140 | self.socket = None 141 | 142 | def _receive(self) -> RSCPDTO: 143 | logger.debug("Waiting for response from " + str(self.ip)) 144 | decrypted_data = None 145 | wait = 0.01 146 | while not decrypted_data: 147 | try: 148 | data = self.socket.recv(self.BUFFER_SIZE) 149 | logger.debug('Received ' + str(len(data)) + ' Bytes') 150 | if len(data) == 0: 151 | self.socket.close() 152 | raise RSCPCommunicationError("Did not receive data from e3dc", logger) 153 | self.rscp_utils = RSCPUtils() 154 | 155 | decrypted_data = self.encrypt_decrypt.decrypt(data) 156 | except BlockingIOError: 157 | logger.debug('Keine Daten empfangen, warte ' + str(wait) + 's') 158 | time.sleep(wait) 159 | wait*=2 160 | if wait > 2: 161 | raise 162 | 163 | 164 | #rawdata = binascii.hexlify(decrypted_data) 165 | #logger.debug('Response RAW: ' + str(rawdata)) 166 | rscp_dto = self.rscp_utils.decode_data(decrypted_data) 167 | logger.debug("Received DTO Type: " + rscp_dto.type.name + ", DTO Tag: " + rscp_dto.tag.name) 168 | return rscp_dto 169 | -------------------------------------------------------------------------------- /e3dc/rscp_helper.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rxhan/RSCPGui/90a7315be0c5c9f3f14eb37822eb1dd781050c09/e3dc/rscp_helper.py -------------------------------------------------------------------------------- /e3dc/rscp_type.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | _data_type_mapping = { 4 | "Bool": "?", 5 | "Char8": "b", 6 | "UChar8": "B", 7 | "Int16": "h", 8 | "Uint16": "H", 9 | "Int32": "i", 10 | "Uint32": "I", 11 | "Int64": "q", 12 | "Uint64": "Q", 13 | "Float32": "f", 14 | "Double64": "d", 15 | "Bitfield": "s", 16 | "CString": "s", 17 | "Container": "s", 18 | "ByteArray": "r", 19 | "Error": "i", 20 | "Nil": None, 21 | } 22 | 23 | 24 | class PM_TYPE(Enum): 25 | UNDEFINED = 0 26 | ROOT = 1 27 | ADDITIONAL = 2 28 | ADDITIONAL_PRODUCTION = 3 29 | ADDITIONAL_CONSUMPTION = 4 30 | FARM = 5 31 | UNUSED = 6 32 | WALLBOX = 7 33 | FARM_ADDITIONAL = 8 34 | 35 | 36 | class PM_MODE(Enum): 37 | ACTIVE = 0 38 | PASSIVE = 1 39 | DIAGNOSE = 2 40 | ERROR_ACTIVE = 3 41 | ERROR_PASSIVE = 4 42 | UNKNOWN_65 = 65 43 | UNKNOWN_81 = 81 44 | 45 | 46 | class PM_ACTIVE_PHASES(Enum): 47 | PHASE_100 = 1 48 | PHASE_010 = 2 49 | PHASE_110 = 3 50 | PHASE_001 = 4 51 | PHASE_101 = 5 52 | PHASE_011 = 6 53 | PHASE_111 = 7 54 | 55 | 56 | class PVI_TYPE(Enum): 57 | SOLU = 1 58 | KACO = 2 59 | E3DC_E = 3 60 | UNKNOWN_6 = 6 # Bei H85-Systemen gesehen 61 | 62 | 63 | class PVI_SYSTEM_MODE(Enum): 64 | IDLE = 0 65 | NORMAL = 1 66 | GRIDCHARGE = 2 67 | BACKUPPOWER = 3 68 | 69 | 70 | class PVI_POWER_MODE(Enum): 71 | ON = 1 72 | OFF = 0 73 | ON_FORCE = 101 74 | OFF_FORCE = 100 75 | 76 | 77 | class EMS_GENERATOR_STATE(Enum): 78 | IDLE = 0x00 79 | HEATUP = 0x01 80 | HEATUPDONE = 0x02 81 | STARTING = 0x03 82 | STARTINGPAUSE = 0x04 83 | RUNNING = 0x05 84 | STOPPING = 0x06 85 | STOPPED = 0x07 86 | RELAISCONTROLMODE = 0x10 87 | NO_GENERATOR = 0xFF 88 | 89 | 90 | class EMS_COUPLING_MODE(Enum): 91 | DC = 0 92 | DC_MULTIWR = 1 93 | AC = 2 94 | HYBRID = 3 95 | ISLAND = 4 96 | 97 | 98 | class EMS_SET_POWER_MODE(Enum): 99 | NORMAL = 0 100 | IDLE = 1 101 | ENTLADEN = 2 102 | LADEN = 3 103 | NETZLADEN = 4 104 | 105 | 106 | class EMS_EMERGENCY_POWER_STATUS(Enum): 107 | NOT_POSSIBLE = 0x00 108 | ACTIVE = 0x01 109 | NOT_ACTIVE = 0x02 110 | NOT_AVAILABLE = 0x03 111 | SWITCH_IN_ISLAND_STATE = 0x04 112 | 113 | 114 | class EMS_SET_EMERGENCY_POWER(Enum): 115 | NORMAL_GRID_MODE = 0x00 116 | EMERGENCY_MODE = 0x01 117 | ISLAND_NO_POWER_MODE = 0x02 118 | 119 | 120 | class WB_MODE(Enum): 121 | NONE = 0 122 | LOADING = 144 # 00001001 123 | NOT_LOADING = 128 # 00000001 124 | 125 | 126 | class WB_TYPE(Enum): 127 | E3DC = 1 128 | EASYCONNECT = 2 129 | 130 | 131 | class RSCP_USER_LEVEL(Enum): 132 | NO_AUTH = 0 133 | USER = 10 134 | INSTALLER = 20 135 | PARTNER = 30 136 | E3DC = 40 137 | E3DC_ADMIN = 50 138 | E3DC_ROOT = 60 139 | 140 | 141 | class UM_UPDATE_STATUS(Enum): 142 | IDLE = 0x00 143 | UPDATE_CHECK_RUNNING = 0x01 144 | UPDATING_MODULES_AND_FILES = 0x02 145 | UPDATING_HARDWARE = 0x03 146 | 147 | 148 | class ERROR_CODE(Enum): 149 | ERR_NOT_HANDLED = 0x01 150 | ERR_ACCESS_DENIED = 0x02 151 | ERR_FORMAT = 0x03 152 | ERR_AGAIN = 0x04 153 | RSCP_ERR_OUT_OF_BOUNDS = 0x05 154 | RSCP_ERR_NOT_AVAILABLE = 0x06 155 | RSCP_ERR_UNKNOWN_TAG = 0x07 156 | RSCP_ERR_ALREADY_IN_USE = 0x08 157 | UNEXPECTED = 0xFFFFFFFF 158 | ERR_UNKNOWN = -1 159 | 160 | 161 | class RSCPType(Enum): 162 | Nil = 0x00 163 | Bool = 0x01 164 | Char8 = 0x02 165 | UChar8 = 0x03 166 | Int16 = 0x04 167 | Uint16 = 0x05 168 | Int32 = 0x06 169 | Uint32 = 0x07 170 | Int64 = 0x08 171 | Uint64 = 0x09 172 | Float32 = 0x0A 173 | Double64 = 0x0B 174 | Bitfield = 0x0C 175 | CString = 0x0D 176 | Container = 0x0E 177 | Timestamp = 0x0F 178 | ByteArray = 0x10 179 | Error = 0xFF 180 | 181 | @property 182 | def mapping(self): 183 | return _data_type_mapping[self.name] 184 | -------------------------------------------------------------------------------- /e3dcwebgui.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | logger = logging.getLogger(__name__) 4 | 5 | import hashlib 6 | 7 | import math 8 | import struct 9 | import threading 10 | import traceback 11 | 12 | from e3dc._rscp_utils import RSCPUtils 13 | from e3dc.e3dc import E3DC 14 | from e3dc.rscp_helper import rscp_helper 15 | 16 | try: 17 | import thread 18 | except ImportError: 19 | import _thread as thread 20 | import time 21 | 22 | import websocket 23 | 24 | from e3dc._rscp_dto import RSCPDTO 25 | from e3dc.rscp_tag import RSCPTag 26 | from e3dc.rscp_type import RSCPType 27 | 28 | 29 | class E3DCWebGui(rscp_helper): 30 | timeout = 5 31 | timeout_connect = 10 32 | autoreconnect = True 33 | 34 | def __init__(self, username, password, identifier, url = None): 35 | self.e3dc = E3DCWeb(username, password, identifier, url) 36 | self.connect() 37 | 38 | def connect(self): 39 | if self.e3dc.conid is None: 40 | self._wsthread = threading.Thread(target=self.e3dc.start_ws, args = ()) 41 | self._wsthread.start() 42 | else: 43 | logger.debug('Eine erneute Webverbindung ist nicht möglich') 44 | 45 | def reconnect(self): 46 | self.e3dc.close_ws() 47 | self.connect() 48 | 49 | def __del__(self): 50 | del self.e3dc 51 | 52 | def get_data(self, requests, raw=False, block=True, waittime=None): 53 | start = time.time() 54 | while not self.e3dc.connected: 55 | if (time.time() - start) > self.timeout_connect or self.e3dc.lasterror is not None: 56 | error = str(self.e3dc.lasterror) if self.e3dc.lasterror is not None else '' 57 | if self.autoreconnect: 58 | self.reconnect() 59 | raise Exception('WebGui Verbindungsaufbau fehlgeschlagen, Timeout: ' + error) 60 | time.sleep(0.1) 61 | 62 | r = self.e3dc.getRSCPToServer(requests) 63 | self.e3dc.register_next_response() 64 | try: 65 | self.e3dc.send_data(r, waittime=waittime) 66 | start = time.time() 67 | while self.e3dc.next_response: 68 | if (time.time() - start) > self.timeout or self.e3dc.lasterror is not None: 69 | error = str(self.e3dc.lasterror) if self.e3dc.lasterror is not None else '' 70 | raise Exception('WebGui Datenabfrage fehlgeschlagen, Timeout: ' + error) 71 | time.sleep(0.1) 72 | 73 | return self.e3dc.next_response_data 74 | except websocket._exceptions.WebSocketConnectionClosedException as e: 75 | if self.autoreconnect: 76 | self.reconnect() 77 | logger.error('Verbindung zu Websocket unterbrochen, versuche Verbindung wiederherzustellen: ' + str(e)) 78 | return self.get_data(requests, raw, block) 79 | 80 | 81 | class E3DCWeb(E3DC): 82 | def __init__(self, username, password, identifier, url = None): 83 | logger.debug('Initialisiere E3DC-Websockets') 84 | if not url: 85 | url = 'wss://s10.e3dc.com/ws' 86 | self.password = password 87 | self.username = username 88 | self.rscp_utils = RSCPUtils() 89 | self.identifier = identifier 90 | self.url = url 91 | logger.debug('Init abgeschlossen') 92 | 93 | lasterror = None 94 | conid = None 95 | 96 | server_connection_id = None 97 | server_auth_level = None 98 | info_serial_number = None 99 | server_type = None 100 | ws = None 101 | 102 | next_response = None 103 | next_response_data = None 104 | 105 | def get_connected(self): 106 | if self.server_auth_level == 10 and self.server_connection_id and self.server_auth_level and self.identifier and self.conid: 107 | return True 108 | else: 109 | return False 110 | 111 | def set_connected(self, value): 112 | if value == False: 113 | self.server_auth_level = None 114 | self.server_connection_id = None 115 | self.server_auth_level = None 116 | self.ws = None 117 | self.server_type = None 118 | self.conid = None 119 | 120 | connected = property(get_connected, set_connected) 121 | 122 | 123 | def register_next_response(self): 124 | self.next_response = True 125 | self.next_response_data = None 126 | 127 | def getWeblogin(self): 128 | r = RSCPDTO(RSCPTag.SERVER_REQ_NEW_VIRTUAL_CONNECTION, rscp_type=RSCPType.Container) 129 | 130 | r += RSCPDTO(RSCPTag.SERVER_USER, RSCPType.CString, self.username) 131 | pass_md5 = hashlib.md5() 132 | pass_md5.update(self.password.encode('utf-8')) 133 | password = pass_md5.hexdigest() 134 | r += RSCPDTO(RSCPTag.SERVER_PASSWD, RSCPType.CString, password) 135 | 136 | r += RSCPDTO(RSCPTag.SERVER_IDENTIFIER, RSCPType.CString, self.identifier) 137 | r += RSCPDTO(RSCPTag.SERVER_TYPE, RSCPType.Int32, 4) 138 | r += RSCPDTO(RSCPTag.SERVER_HASH_CODE, RSCPType.Int32, 1234567890) 139 | 140 | return [r] 141 | 142 | def interpreter_serverdata(self, data): 143 | if not isinstance(data, list): 144 | data = [data] 145 | 146 | requests = [] 147 | for res in data: 148 | if res.name == 'SERVER_REGISTER_CONNECTION': 149 | logger.debug(res.name) 150 | self.server_connection_id = res['SERVER_CONNECTION_ID'].data 151 | self.server_auth_level = res['SERVER_AUTH_LEVEL'].data 152 | self.server_type = res['SERVER_TYPE'].data 153 | 154 | r = RSCPDTO(tag=RSCPTag.SERVER_CONNECTION_REGISTERED, rscp_type=RSCPType.Container) 155 | 156 | r += RSCPDTO(tag=RSCPTag.SERVER_CONNECTION_ID, rscp_type=RSCPType.Int64, 157 | data=self.server_connection_id) 158 | r += RSCPDTO(tag=RSCPTag.SERVER_AUTH_LEVEL, rscp_type=RSCPType.UChar8, 159 | data=self.server_auth_level) 160 | requests.append(r) 161 | 162 | if res.name == 'SERVER_UNREGISTER_CONNECTION': 163 | logger.debug(res.name) 164 | self.server_connection_id = None 165 | self.server_auth_level = None 166 | r = self.getWeblogin() 167 | requests += r 168 | 169 | elif res.name == 'SERVER_CUSTOM_START': 170 | logger.debug(res.name) 171 | r = RSCPDTO(tag=RSCPTag.SERVER_CUSTOM_ANSWER, rscp_type=RSCPType.Char8, data=1) 172 | requests.append(r) 173 | 174 | elif res.name == 'SERVER_REQ_RSCP_CMD': 175 | if res['SERVER_RSCP_DATA']: 176 | p = [] 177 | rscp_data = res['SERVER_RSCP_DATA'] 178 | 179 | if self.next_response: 180 | if isinstance(rscp_data.data, RSCPDTO): 181 | self.next_response_data = rscp_data.data 182 | else: 183 | self.next_response_data = rscp_data 184 | self.next_response = None 185 | if 'INFO_SERIAL_NUMBER' in rscp_data: 186 | self.info_serial_number = rscp_data['INFO_SERIAL_NUMBER'].data 187 | r = self.getWeblogin() 188 | requests += r 189 | 190 | if 'INFO_REQ_IP_ADDRESS' in rscp_data: 191 | p.append(RSCPDTO(RSCPTag.INFO_IP_ADDRESS, rscp_type=RSCPType.CString, data='0.0.0.0')) 192 | if 'INFO_REQ_SUBNET_MASK' in rscp_data: 193 | p.append(RSCPDTO(RSCPTag.INFO_SUBNET_MASK, rscp_type=RSCPType.CString, data='0.0.0.0')) 194 | if 'INFO_REQ_GATEWAY' in rscp_data: 195 | p.append(RSCPDTO(RSCPTag.INFO_GATEWAY, rscp_type=RSCPType.CString, data='0.0.0.0')) 196 | if 'INFO_REQ_DNS' in rscp_data: 197 | p.append(RSCPDTO(RSCPTag.INFO_DNS, rscp_type=RSCPType.CString, data='0.0.0.0')) 198 | if 'INFO_REQ_DHCP_STATUS' in rscp_data: 199 | p.append(RSCPDTO(RSCPTag.INFO_DHCP_STATUS, rscp_type=RSCPType.Bool, data=False)) 200 | 201 | if 'INFO_REQ_TIME' in rscp_data: 202 | # TODO: Zeitstempel korrekt bilden mit Berücksichtigung der Zeitzone 203 | current_time = time.time() 204 | seconds = math.ceil(current_time) 205 | nanoseconds = round((current_time - int(current_time)) * 1000) 206 | ts = struct.pack(' 0: 234 | requests.append(self.getRSCPToServer(p)) 235 | 236 | elif res.name == 'SERVER_REQ_PING': 237 | logger.debug(res.name) 238 | r = RSCPDTO(tag=RSCPTag.SERVER_PING) 239 | requests.append(r) 240 | 241 | return requests 242 | 243 | def getRSCPToServer(self, p): 244 | if not isinstance(p, list): 245 | p = [p] 246 | 247 | payload = b'' 248 | for payload_element in p: 249 | if isinstance(payload_element, RSCPTag): 250 | x = RSCPDTO(payload_element) 251 | else: 252 | x = payload_element 253 | payload += self.rscp_utils.encode_data(x) 254 | 255 | payload = self.rscp_utils.encode_frame(payload) 256 | 257 | r = RSCPDTO(tag=RSCPTag.SERVER_REQ_RSCP_CMD, rscp_type=RSCPType.Container) 258 | r += RSCPDTO(tag=RSCPTag.SERVER_CONNECTION_ID, rscp_type=RSCPType.Int64, data=self.server_connection_id) 259 | r += RSCPDTO(tag=RSCPTag.SERVER_AUTH_LEVEL, rscp_type=RSCPType.UChar8, data=self.server_auth_level) 260 | r += RSCPDTO(tag=RSCPTag.SERVER_RSCP_DATA_LEN, rscp_type=RSCPType.Int32, data=len(payload)) 261 | r += RSCPDTO(tag=RSCPTag.SERVER_RSCP_DATA, rscp_type=RSCPType.ByteArray, data=payload) 262 | return r 263 | 264 | def send_data(self, r, ws = None, waittime=None): 265 | if not ws: 266 | ws = self.ws 267 | logger.debug('Sende Daten: ' + str(r)) 268 | dataframe = self.rscp_utils.encode_data(r) 269 | bindat = self.rscp_utils.encode_frame(dataframe, crc=True) 270 | logger.debug('Sende Daten ' + str(len(bindat))) 271 | ws.send(bindat, websocket.ABNF.OPCODE_BINARY) 272 | 273 | def close_ws(self): 274 | if self.ws and self.conid: 275 | self.ws.close() 276 | 277 | 278 | def start_ws(self): 279 | if self.connected: 280 | conid = 'ConID: ' + self.conid 281 | logger.warning(conid + ' - Websocket-Verbindung besteht bereits, eine erneute Verbindung ist nicht möglich') 282 | return False 283 | 284 | self.conid = str(round(time.time(),2)) 285 | conid = 'ConID: ' + self.conid 286 | 287 | def on_message(ws, message): 288 | try: 289 | data = self.rscp_utils.decode_server_data(message) 290 | res = self.interpreter_serverdata(data) 291 | for r in res: 292 | self.send_data(r, ws) 293 | self.lasterror = None 294 | except Exception as e: 295 | self.lasterror = e 296 | logger.exception(conid + ' - Fehler beim Verarbeiten der Daten : ' + str(e)) 297 | 298 | def on_error(ws, error): 299 | self.lasterror = error 300 | logger.error(conid + ' - Verbindungsfehler ' + str(error)) 301 | 302 | def on_close(ws): 303 | self.lasterror = 'Connection closed' 304 | self.connected = False 305 | logger.info(conid + ' - Verbindung geschlossen') 306 | 307 | #websocket.enableTrace(True) 308 | ws = websocket.WebSocketApp(self.url, 309 | on_message=on_message, 310 | on_error=on_error, 311 | on_close=on_close) 312 | 313 | self.ws = ws 314 | logger.debug(conid + ' - Starte Websocket-Verbindung mit ' + self.url) 315 | ws.run_forever() 316 | logger.debug(conid + ' - Websocket-Verbindung beendet') 317 | self.conid = None 318 | 319 | def __del__(self): 320 | self.ws.close() 321 | -------------------------------------------------------------------------------- /export.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | logger = logging.getLogger(__name__) 4 | 5 | logger.debug('Programmstart') 6 | 7 | from gui import ExportFrame 8 | import wx.lib.agw.customtreectrl as CT 9 | import wx 10 | 11 | class CustomTreeCtrl(CT.CustomTreeCtrl): 12 | _checked_items = [] 13 | 14 | def DeleteAllItems(self): 15 | self._checked_items = [] 16 | CT.CustomTreeCtrl.DeleteAllItems(self) 17 | 18 | def GetCheckedItems(self): 19 | return self._checked_items 20 | 21 | def GetAllItems(self, item = None): 22 | if not item: 23 | item = self.GetRootItem() 24 | items = [] 25 | (child, cookie) = self.GetFirstChild(item) 26 | while child and child.IsOk(): 27 | items.append(child) 28 | items += self.GetAllItems(child) 29 | 30 | (child, cookie) = self.GetNextChild(item, cookie) 31 | 32 | return items 33 | 34 | def CheckItem2(self, item, checked=True, torefresh=False): 35 | dat = self.GetItemData(item) 36 | if dat is not None and not isinstance(dat, list) and not isinstance(dat, dict): 37 | font = self.GetItemFont(item) 38 | if checked: 39 | self.SetItemFont(item, font.Bold()) 40 | if item not in self._checked_items: 41 | self._checked_items.append(item) 42 | elif not checked: 43 | font.SetWeight(wx.FONTWEIGHT_NORMAL) 44 | self.SetItemFont(item, font) 45 | if item in self._checked_items: 46 | self._checked_items.remove(item) 47 | 48 | CT.CustomTreeCtrl.CheckItem2(self, item, checked, torefresh) 49 | 50 | 51 | class E3DCExport(ExportFrame): 52 | _UploadStarted = False 53 | 54 | 55 | def __init__(self, parent, paths = None, names = None): 56 | if not paths: 57 | self._paths = [] 58 | else: 59 | self._paths = paths 60 | 61 | self._customNames = {} 62 | 63 | self._parent = parent 64 | wx.Frame.__init__ ( self, parent, id = wx.ID_ANY, title = wx.EmptyString, pos = wx.DefaultPosition, size = wx.Size( 522,609 ), style = wx.DEFAULT_FRAME_STYLE|wx.TAB_TRAVERSAL ) 65 | 66 | self.SetSizeHints(wx.DefaultSize, wx.DefaultSize) 67 | 68 | bSizer181 = wx.BoxSizer(wx.VERTICAL) 69 | 70 | self.tcUpload = CustomTreeCtrl(self, wx.ID_ANY, wx.DefaultPosition, wx.Size(500, 400), wx.TR_DEFAULT_STYLE) 71 | 72 | bSizer181.Add(self.tcUpload, 0, wx.ALL, 5) 73 | 74 | fgSizer35 = wx.FlexGridSizer(0, 2, 0, 0) 75 | fgSizer35.SetFlexibleDirection(wx.BOTH) 76 | fgSizer35.SetNonFlexibleGrowMode(wx.FLEX_GROWMODE_SPECIFIED) 77 | 78 | bSizer19 = wx.BoxSizer(wx.VERTICAL) 79 | 80 | fgSizer35.Add(bSizer19, 1, wx.EXPAND, 5) 81 | 82 | fgSizer36 = wx.FlexGridSizer(0, 2, 0, 0) 83 | fgSizer36.SetFlexibleDirection(wx.BOTH) 84 | fgSizer36.SetNonFlexibleGrowMode(wx.FLEX_GROWMODE_SPECIFIED) 85 | 86 | self.m_staticText197 = wx.StaticText(self, wx.ID_ANY, u"Data", wx.DefaultPosition, wx.DefaultSize, 0) 87 | self.m_staticText197.Wrap(-1) 88 | 89 | fgSizer36.Add(self.m_staticText197, 0, wx.ALL, 5) 90 | 91 | self.txtUploadData = wx.TextCtrl(self, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.Size(300, -1), 0) 92 | fgSizer36.Add(self.txtUploadData, 0, wx.ALL, 5) 93 | 94 | self.m_staticText198 = wx.StaticText(self, wx.ID_ANY, u"Name", wx.DefaultPosition, wx.DefaultSize, 0) 95 | self.m_staticText198.Wrap(-1) 96 | 97 | fgSizer36.Add(self.m_staticText198, 0, wx.ALL, 5) 98 | 99 | self.txtUploadName = wx.TextCtrl(self, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.Size(300, -1), 0) 100 | fgSizer36.Add(self.txtUploadName, 0, wx.ALL, 5) 101 | 102 | self.m_staticText199 = wx.StaticText(self, wx.ID_ANY, u"Pfad", wx.DefaultPosition, wx.DefaultSize, 0) 103 | self.m_staticText199.Wrap(-1) 104 | 105 | fgSizer36.Add(self.m_staticText199, 0, wx.ALL, 5) 106 | 107 | self.txtUploadPath = wx.TextCtrl(self, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.Size(300, -1), 0) 108 | fgSizer36.Add(self.txtUploadPath, 0, wx.ALL, 5) 109 | 110 | self.m_staticText200 = wx.StaticText(self, wx.ID_ANY, u"Bezeichner", wx.DefaultPosition, wx.DefaultSize, 0) 111 | self.m_staticText200.Wrap(-1) 112 | 113 | fgSizer36.Add(self.m_staticText200, 0, wx.ALL, 5) 114 | 115 | self.txtUploadCustom = wx.TextCtrl(self, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.Size(300, -1), 0) 116 | fgSizer36.Add(self.txtUploadCustom, 0, wx.ALL, 5) 117 | 118 | fgSizer35.Add(fgSizer36, 1, wx.EXPAND, 5) 119 | 120 | bSizer181.Add(fgSizer35, 1, wx.EXPAND, 5) 121 | 122 | self.bSave = wx.Button(self, wx.ID_ANY, u"speichern", wx.DefaultPosition, wx.DefaultSize, 0, 123 | wx.DefaultValidator, u"UPLOAD") 124 | bSizer181.Add(self.bSave, 0, wx.ALL, 5) 125 | 126 | self.SetSizer(bSizer181) 127 | self.Layout() 128 | 129 | self.Centre(wx.BOTH) 130 | 131 | # Connect Events 132 | self.tcUpload.Bind(wx.EVT_TREE_SEL_CHANGED, self.tcUploadOnSelChanged) 133 | self.bSave.Bind(wx.EVT_BUTTON, self.bSaveOnClick) 134 | 135 | self.tcUpload.Bind(CT.EVT_TREE_ITEM_CHECKED, self.bUploadLoadItemChecked) 136 | self.tcUpload.Bind(wx.EVT_TREE_SEL_CHANGING, self.tcUploadOnSelChanging) 137 | 138 | self.txtUploadData.Enable(False) 139 | self.txtUploadName.Enable(False) 140 | self.txtUploadPath.Enable(False) 141 | 142 | self.loadData() 143 | 144 | if names: 145 | self.setCustomNames(names) 146 | 147 | def bSaveOnClick( self, event ): 148 | self.saveCustomName() 149 | self.Close() 150 | 151 | def bUploadLoadItemChecked(self, event): 152 | item = event.GetItem() 153 | checked = item.GetValue() 154 | 155 | self.tcUpload.CheckItem2(item, checked) 156 | self.tcUpload.AutoCheckChild(item, checked) 157 | 158 | logger.debug('Checked Items: ' + str(len(self.tcUpload.GetCheckedItems()))) 159 | 160 | def loadData(self): 161 | data = self._parent.sammle_data(anon = False) 162 | 163 | self.tcUpload.DeleteAllItems() 164 | 165 | logger.debug('Lade Datenbaum') 166 | try: 167 | 168 | def loadInCtrl(data, parent: wx.TreeItemId = None, name: str = None): 169 | ct_type = 1 170 | if parent == None: 171 | new = self.tcUpload.AddRoot('E3DC',ct_type=ct_type) 172 | else: 173 | new = self.tcUpload.AppendItem(parent, 174 | text=name, 175 | data=data, 176 | ct_type=ct_type) 177 | for path in self._paths: 178 | if self.getUploadPath(new) == path: 179 | self.tcUpload.CheckItem2(new, checked=True) 180 | if isinstance(data, list): 181 | i = 0 182 | for d in data: 183 | loadInCtrl(d, new, name = str(i)) 184 | i += 1 185 | elif isinstance(data, dict): 186 | for name in data.keys(): 187 | loadInCtrl(data[name], new, name = str(name)) 188 | 189 | #reduziert = {'EMS_DATA': data['EMS_DATA'], 'BAT_DATA': data['BAT_DATA']} 190 | loadInCtrl(data) 191 | logger.debug('Datenbaum erfolgreich geladen') 192 | 193 | self.tcUpload.ExpandAll() 194 | except: 195 | logger.exception('Datenbaum konnte nicht geladen werden') 196 | 197 | def tcUploadOnSelChanged( self, event ): 198 | ret: wx.TreeItemId = self.tcUpload.GetSelection() 199 | 200 | data = self.tcUpload.GetItemData(ret) 201 | text = self.tcUpload.GetItemText(ret) 202 | logger.debug('Item ' + text + ' mit Data ' + str(data) + ' markiert') 203 | if data is not None and not isinstance(data, dict) and not isinstance(data, list): 204 | self.txtUploadData.SetValue(str(data)) 205 | elif data is None: 206 | self.txtUploadData.SetValue('none') 207 | else: 208 | self.txtUploadData.SetValue(' - ') 209 | self.txtUploadName.SetValue(text) 210 | 211 | path = self.getUploadPath(ret) 212 | self.txtUploadPath.SetValue(path) 213 | 214 | if path in self._customNames: 215 | self.txtUploadCustom.SetValue(self._customNames[path]) 216 | else: 217 | self.txtUploadCustom.SetValue(path) 218 | 219 | def tcUploadOnSelChanging( self, event ): 220 | self.saveCustomName() 221 | 222 | def saveCustomName(self): 223 | ret: wx.TreeItemId = self.tcUpload.GetSelection() 224 | custom = self.txtUploadCustom.GetValue() 225 | path = self.getUploadPath(ret) 226 | 227 | if path not in self._customNames or self._customNames[path] != custom: 228 | self.changeCustomName(ret, custom, path) 229 | 230 | if custom != path: 231 | data = self.tcUpload.GetItemData(ret) 232 | if isinstance(data, dict) or isinstance(data, list): 233 | res = wx.MessageBox( 234 | 'Der Bezeichner eines Knoten wurde geändert, soll für alle untergeordneten markierten Einträge der Knotenname angepasst werden?', 235 | 'Knotenname geändert', wx.YES_NO) 236 | if res == wx.YES: 237 | logger.debug('Unterknoten von ' + path + ' werden geändert') 238 | changed = self.changeCustomNames(ret, custom) 239 | logger.debug('Es wurden ' + str(changed) + ' Einträge geändert') 240 | 241 | def changeCustomName(self, item, custom, path=None): 242 | if not path: 243 | path = self.getUploadPath(item) 244 | 245 | if path not in self._customNames or self._customNames[path] != custom: 246 | logger.debug('Individueller Bezeichner für ' + path + ' geändert auf ' + custom) 247 | self._customNames[path] = custom 248 | 249 | if custom != path: 250 | self.tcUpload.SetItemTextColour(item, wx.Colour( 0xff, 0x80, 0x80 )) 251 | else: 252 | self.tcUpload.SetItemTextColour(item, wx.Colour( 0x00, 0x00, 0x00 )) 253 | 254 | 255 | def changeCustomNames(self, item, custom, all=True): 256 | path = self.getUploadPath(item) 257 | name = self.tcUpload.GetItemText(item) 258 | 259 | replace_path = path 260 | changed = 0 261 | 262 | (child, cookie) = self.tcUpload.GetFirstChild(item) 263 | while child and child.IsOk(): 264 | p = self.getUploadPath(child) 265 | if all or (p not in self._customNames or self._customNames[p] == p): 266 | newname = custom + p[len(replace_path):] 267 | self.changeCustomName(child, newname, p) 268 | changed += 1 269 | data = self.tcUpload.GetItemData(child) 270 | if isinstance(data, dict) or isinstance(data, list): 271 | changed += self.changeCustomNames(child, newname) 272 | 273 | (child, cookie) = self.tcUpload.GetNextChild(item, cookie) 274 | 275 | return changed 276 | 277 | 278 | def getUploadPath(self, item): 279 | try: 280 | path = self.tcUpload.GetItemText(item) 281 | parent = self.tcUpload.GetItemParent(item) 282 | if parent: 283 | path = self.getUploadPath(parent) + '/' + path 284 | except: 285 | logger.exception('Fehler beim Ermitteln des Pfades: ') 286 | print(item, type(item)) 287 | return '' 288 | 289 | return path 290 | 291 | def getExportPaths(self): 292 | items = self.tcUpload.GetCheckedItems() 293 | 294 | paths = [] 295 | for item in items: 296 | paths.append(self.getUploadPath(item)) 297 | 298 | return paths 299 | 300 | def getCustomNames(self): 301 | items = self.tcUpload.GetCheckedItems() 302 | 303 | result = {} 304 | for item in items: 305 | path = self.getUploadPath(item) 306 | if path in self._customNames: 307 | result[path] = self._customNames[path] 308 | else: 309 | result[path] = path 310 | return result 311 | 312 | def setCustomNames(self, value): 313 | items = self.tcUpload.GetAllItems() 314 | logger.debug('Alle items ' + str(len(items))) 315 | for item in items: 316 | path = self.getUploadPath(item) 317 | if path in value: 318 | self.changeCustomName(item, value[path], path) 319 | 320 | 321 | 322 | 323 | 324 | -------------------------------------------------------------------------------- /exportservice.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rxhan/RSCPGui/90a7315be0c5c9f3f14eb37822eb1dd781050c09/exportservice.py -------------------------------------------------------------------------------- /images/RSCPGUI_Assistant.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rxhan/RSCPGui/90a7315be0c5c9f3f14eb37822eb1dd781050c09/images/RSCPGUI_Assistant.PNG -------------------------------------------------------------------------------- /images/RSCPGUI_BAT.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rxhan/RSCPGui/90a7315be0c5c9f3f14eb37822eb1dd781050c09/images/RSCPGUI_BAT.png -------------------------------------------------------------------------------- /images/RSCPGUI_EMS.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rxhan/RSCPGui/90a7315be0c5c9f3f14eb37822eb1dd781050c09/images/RSCPGUI_EMS.png -------------------------------------------------------------------------------- /images/RSCPGUI_Ladeeinstellungen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rxhan/RSCPGui/90a7315be0c5c9f3f14eb37822eb1dd781050c09/images/RSCPGUI_Ladeeinstellungen.png -------------------------------------------------------------------------------- /images/RSCPGUI_Wechselrichter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rxhan/RSCPGui/90a7315be0c5c9f3f14eb37822eb1dd781050c09/images/RSCPGUI_Wechselrichter.png -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import argparse 3 | import socket 4 | import sys 5 | 6 | parser = argparse.ArgumentParser(description='Ruft Daten von E3DC-Systemen mittels RSCP ab') 7 | parser.add_argument('-e', '--export', const=True, default=False, nargs='?', 8 | help='Exportstart mit Programmstart') 9 | parser.add_argument('-i', '--hide', action='store_true', 10 | help='Programm verstecken') 11 | parser.add_argument("-c", "--console", const=True, default=False, nargs='?', help='Verwendung als Konsolenprogramm, sonst mit Oberfläche') 12 | parser.add_argument("-f", "--logfile", const='rscpgui.log', default=None, nargs='?', help='Ausgabe in eine Logdatei (Default bei Oberfläche)') 13 | parser.add_argument("-v", "--verbose", const='INFO', choices=logging._nameToLevel.keys(), default='ERROR', nargs='?', help='Anpassen des Loglevels') 14 | parser.add_argument("-l", "--logconsole", action='store_true', help='Logausgabe auch auf der Console (nur sinnvoll ohne -c)') 15 | parser.add_argument("-p", "--portal", action='store_true', help='Sende Batteriedaten an Portal') 16 | 17 | 18 | args = parser.parse_args() 19 | loglevel = args.verbose 20 | 21 | # Patch für schnellere Namensauflösung in IPv6 - Netzwerken: 22 | 23 | orig_getaddrinfo = socket.getaddrinfo 24 | 25 | def _getaddrinfo(host, port, family=0, type=0, proto=0, flags=0): 26 | return orig_getaddrinfo(host, port, socket.AF_INET, type, proto, flags) 27 | 28 | socket.getaddrinfo = _getaddrinfo 29 | 30 | def setLoglevel(loglevel, filename = None, console = True, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'): 31 | loggernames = [__name__, 'rscpguiframe','rscpguimain','rscpguiconsole','export','e3dcwebgui','e3dc','assistant'] 32 | for name in loggernames: 33 | l = logging.getLogger(name) 34 | l.setLevel(loglevel) 35 | 36 | formatter = logging.Formatter(format) 37 | 38 | if console: 39 | ch = logging.StreamHandler() 40 | ch.setLevel(loglevel) 41 | 42 | ch.setFormatter(formatter) 43 | l.addHandler(ch) 44 | 45 | if filename: 46 | ch = logging.FileHandler(filename) 47 | ch.setLevel(loglevel) 48 | ch.setFormatter(formatter) 49 | l.addHandler(ch) 50 | 51 | if not args.console and not args.logfile: 52 | logfile = 'rscpgui.log' 53 | else: 54 | logfile = args.logfile 55 | 56 | if args.console or args.logconsole: 57 | console = True 58 | else: 59 | console = False 60 | 61 | setLoglevel(args.verbose, logfile, console) 62 | 63 | logger = logging.getLogger(__name__) 64 | logger.debug('Programmstart') 65 | 66 | try: 67 | import wx 68 | except: 69 | logger.warning('wxPython steht nicht zur Verfügung, Programm beschränkt sich auf die Console') 70 | 71 | logger.info('Lade Module') 72 | 73 | if 'wx' in sys.modules.keys() and not args.console: 74 | from rscpguiframe import RSCPGuiFrame 75 | 76 | logger.info('Module geladen, initialisiere App') 77 | app = wx.App() 78 | app.InitLocale() 79 | logger.info('App initialisiert, lade Fenster') 80 | g = RSCPGuiFrame(None, args) 81 | logger.info('Fenster geladen') 82 | if not args.hide: 83 | logger.info('zeichne Fenster') 84 | if g.assistantFrame: 85 | g.assistantFrame.Show() 86 | g.assistantFrame.SetFocus() 87 | else: 88 | g.Show() 89 | 90 | logger.info('Fenster gezeichnet, warte auf Events') 91 | app.MainLoop() 92 | else: 93 | from rscpguiconsole import RSCPGuiConsole 94 | 95 | logger.info('Module geladen, initialisiere Console') 96 | g = RSCPGuiConsole(args) 97 | g.MainLoop() 98 | 99 | logger.info('Programm beendet') -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | py3rijndael~=0.3.3 2 | wxPython==4.2.1 3 | requests==2.31 4 | websocket-client==1.6.3 5 | pytz==2023.3.post1 6 | paho-mqtt==1.6.1 7 | influxdb~=5.3.0 8 | python-telegram-bot==20.5 -------------------------------------------------------------------------------- /rscpe3dc.conf.default.ini: -------------------------------------------------------------------------------- 1 | [Login] 2 | username= 3 | password= 4 | address= 5 | rscppassword= 6 | seriennummer= 7 | websocketaddr= 8 | connectiontype= 9 | autoupdate= 10 | 11 | [Export] 12 | csv= 13 | csvfile= 14 | json= 15 | jsonfile= 16 | mqtt= 17 | mqttbroker= 18 | mqttport= 19 | mqttqos= 20 | mqttretain= 21 | mqttusername= 22 | mqttpassword= 23 | mqttzertifikat= 24 | mqttinsecure= 25 | influx= 26 | influxhost= 27 | influxport= 28 | influxdatenbank= 29 | influxtimeout= 30 | influxname= 31 | http= 32 | httpurl= 33 | intervall= 34 | paths= 35 | pathnames= 36 | -------------------------------------------------------------------------------- /rscpguiconsole.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | logger = logging.getLogger(__name__) 4 | 5 | import os 6 | from rscpguimain import RSCPGuiMain 7 | import time 8 | 9 | 10 | class RSCPGuiConsole(RSCPGuiMain): 11 | def __init__(self, args): 12 | RSCPGuiMain.__init__(self, args) 13 | 14 | def check_e3dcwebgui(self): 15 | while True: 16 | try: 17 | if self.connectiontype == 'web': 18 | try: 19 | if self.gui.e3dc.connected: 20 | self._connected = True 21 | else: 22 | self._connected = False 23 | except: 24 | self._connected = None 25 | elif self.connectiontype == 'direkt': 26 | self._connected = True 27 | else: 28 | self._connected = False 29 | except RuntimeError: 30 | logger.debug('Beende check_e3dcwebgui') 31 | os._exit(1) 32 | except: 33 | self._connected = None 34 | logger.exception('check_e3dcwebgui') 35 | time.sleep(2) 36 | 37 | def MainLoop(self): 38 | if self._args.export or self._args.portal: 39 | g = self.gui 40 | logger.debug('Export bei Programmstart aktiviert') 41 | if self._args.export: 42 | self.StartExport() 43 | 44 | if self._args.portal: 45 | self.sendToPortalMin() 46 | 47 | # Loop forever 48 | try: 49 | self.check_e3dcwebgui() 50 | except: 51 | logger.debug('Beende Programm') 52 | self._AutoExportStarted = False 53 | else: 54 | logger.debug('Nichts zu tun, versuche -h') 55 | 56 | 57 | @property 58 | def gui(self): 59 | if self._gui: 60 | return self._gui 61 | else: 62 | return RSCPGuiMain.gui.fget(self) -------------------------------------------------------------------------------- /rscpguimain.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import math 3 | import os 4 | import traceback 5 | 6 | from e3dc._rscp_dto import RSCPDTO 7 | 8 | logger = logging.getLogger(__name__) 9 | 10 | from socket import setdefaulttimeout 11 | import csv 12 | import re 13 | import random 14 | import base64 15 | import hashlib 16 | import configparser 17 | import time 18 | import json 19 | import requests 20 | import sys 21 | import datetime 22 | import threading 23 | from e3dc._rscp_exceptions import RSCPCommunicationError 24 | from e3dc.rscp_helper import rscp_helper 25 | from e3dc.rscp_tag import RSCPTag 26 | from e3dc.rscp_type import RSCPType 27 | from e3dcwebgui import E3DCWebGui 28 | 29 | try: 30 | import paho.mqtt.client as paho 31 | except: 32 | logger.warning('Paho-Libary (paho) nicht gefunden, MQTT wird nicht zur Verfügung stehen') 33 | 34 | try: 35 | import influxdb 36 | from influxdb.exceptions import InfluxDBClientError 37 | except: 38 | logger.warning('Influxdb-Libary nicht gefunden, Influx wird nicht zur Verfügung stehen') 39 | 40 | try: 41 | import telegram 42 | except Exception as e: 43 | logger.warning('Telegram-Libary (python-telegram-bot) nicht gefunden, Telegram-Benachrichtigungen stehen nicht zur Verfügung') 44 | 45 | try: 46 | import thread 47 | except ImportError: 48 | import _thread as thread 49 | 50 | class E3DCGui(rscp_helper): 51 | pass 52 | 53 | class RSCPGuiMain(): 54 | _extsrcavailable = 0 55 | _gui = None 56 | _connected = None 57 | _mbsSettings = {} 58 | debug = False 59 | _AutoExportStarted = False 60 | _args = None 61 | _updateRunning = None 62 | _try_connect = False 63 | _notificationblocker = {} 64 | _mqttclient = None 65 | _exportcache = None 66 | _dcdc_available = [] 67 | _pm_available = [] 68 | _pvi_available = [] 69 | _wb_available = [] 70 | _noconfigfile = True 71 | 72 | def __init__(self, args): 73 | logger.info('Main initialisiert') 74 | self._args = args 75 | self.clear_values() 76 | self.ConfigFilename = 'rscpe3dc.conf.ini' 77 | 78 | self._config = None 79 | self._gui = None 80 | 81 | self._cached = {} 82 | 83 | setdefaulttimeout(2) 84 | 85 | def clear_values(self): 86 | self._data_bat = [] 87 | self._data_dcdc = [] 88 | self._data_ems = None 89 | self._data_info = None 90 | self._data_pvi = {} 91 | self._data_pm = {} 92 | self._data_wb = [] 93 | 94 | def tinycode(self, key, text, reverse=False): 95 | "(de)crypt stuff" 96 | rand = random.Random(key).randrange 97 | 98 | if reverse: 99 | text = base64.b64decode(text.encode('utf-8')).decode('utf-8', 'ignore') 100 | text = ''.join([chr(ord(elem) ^ rand(256)) for elem in text]) 101 | 102 | if not reverse: 103 | text = base64.b64encode(text.encode('utf-8')).decode('utf-8', 'ignore') 104 | 105 | return text 106 | 107 | def StartExport(self): 108 | self._AutoExportStarted = True 109 | self._autoexportthread = threading.Thread(target=self.StartAutoExport, args=()) 110 | self._autoexportthread.start() 111 | 112 | def StopExport(self): 113 | pass 114 | 115 | @property 116 | def config(self): 117 | if self._config is None: 118 | logger.info('Lade Konfigurationsdatei ' + self.ConfigFilename) 119 | self._config = configparser.ConfigParser() 120 | self._noconfigfile = not os.path.isfile(self.ConfigFilename) 121 | self._config.read(self.ConfigFilename) 122 | 123 | return self._config 124 | 125 | def __setattr__(self, key, value): 126 | if len(key) > 8 and key[0:8] == 'cfgLogin': 127 | name = key[8:] 128 | kat = 'Login' 129 | elif len(key) > 9 and key[0:9] == 'cfgExport': 130 | name = key[9:] 131 | kat = 'Export' 132 | elif len(key) > 15 and key[0:15] == 'cfgNotification': 133 | name = key[15:] 134 | kat = 'Notification' 135 | else: 136 | name = None 137 | kat = None 138 | 139 | if name and kat: 140 | if isinstance(value, dict): 141 | self._cached[name + kat] = value 142 | newvalue = [] 143 | for k in value.keys(): 144 | newvalue.append(k + '|' + value[k]) 145 | value = ','.join(newvalue) 146 | 147 | if isinstance(value, list): 148 | self._cached[name + kat] = value 149 | value = ','.join(value) 150 | 151 | if isinstance(value, int): 152 | value = str(value) 153 | 154 | if kat not in self._config: 155 | self._config[kat] = {} 156 | 157 | 158 | self._config[kat][name] = value 159 | else: 160 | super(RSCPGuiMain, self).__setattr__(key, value) 161 | 162 | 163 | def __getattr__(self, item): 164 | if len(item) > 8 and item[0:8] == 'cfgLogin': 165 | name = item[8:] 166 | kat = 'Login' 167 | elif len(item) > 9 and item[0:9] == 'cfgExport': 168 | name = item[9:] 169 | kat = 'Export' 170 | elif len(item) > 15 and item[0:15] == 'cfgNotification': 171 | name = item[15:] 172 | kat = 'Notification' 173 | else: 174 | raise AttributeError('Wert ' + item + ' existiert nicht (get)') 175 | 176 | if kat in self.config: 177 | if kat == 'Login': 178 | if name in self._config[kat]: 179 | if name == 'password' and self._config[kat][name] != '' and self._config[kat][name][0] == '@': 180 | return self.tinycode('rscpgui', self._config[kat][name][1:], True) 181 | elif name == 'rscppassword' and self._config[kat][name] != '' and self._config[kat][name][0] == '@': 182 | return self.tinycode('rscpgui_rscppass', self._config[kat][name][1:], True) 183 | elif name in ('show_assistant', 'verify_ssl'): 184 | return True if self._config[kat][name].lower() in ('true', '1', 'ja') else False 185 | else: 186 | return self._config[kat][name] 187 | else: 188 | if name == 'websocketaddr': 189 | return 'wss://s10.e3dc.com/ws' 190 | elif name == 'connectiontype': 191 | return 'auto' 192 | elif name in ('show_assistant', 'verify_ssl'): 193 | return True 194 | elif kat == 'Export': 195 | if name in self._config[kat]: 196 | if name in ('csv', 'json', 'mqtt', 'http', 'mqttretain', 'mqttinsecure', 'influx', 'mqttsub'): 197 | return True if self._config[kat][name].lower() in ('true', '1', 'ja') else False 198 | elif name in ('mqttport', 'mqttqos', 'intervall', 'influxport', 'influxtimeout'): 199 | if self._config[kat][name] != '': 200 | return int(self._config[kat][name]) 201 | elif name == 'paths': 202 | #if kat + name not in self._cached: 203 | self._cached[kat + name] = self._config[kat][name].split(',') 204 | return self._cached[kat + name] 205 | elif name == 'mqttpassword' and self._config[kat][name] != '' and self._config[kat][name][0] == '@': 206 | return self.tinycode('rscpgui_mqttpass', self._config[kat][name][1:], True) 207 | elif name == 'pathnames': 208 | items = self._config[kat][name].split(',') 209 | ret = {} 210 | for item in items: 211 | if item: 212 | tmp = item.split('|') 213 | if len(tmp) == 2: 214 | ret[tmp[0]] = tmp[1] 215 | self._cached[kat + name] = ret 216 | 217 | return self._cached[kat + name] 218 | else: 219 | return self._config[kat][name] 220 | elif kat == 'Notification': 221 | if name in self._config[kat]: 222 | if name == 'telegramtoken' and self._config[kat][name] != '' and self._config[kat][name][0] == '@': 223 | return self.tinycode('telegramtoken', self._config[kat][name][1:], True) 224 | elif name in ('telegram'): 225 | return True if self._config[kat][name].lower() in ('true', '1', 'ja') else False 226 | else: 227 | return self._config[kat][name] 228 | 229 | return None 230 | 231 | @property 232 | def connected(self): 233 | return self._connected 234 | 235 | @property 236 | def connectiontype(self): 237 | if isinstance(self._gui, E3DCGui): 238 | return 'direkt' 239 | elif isinstance(self._gui, E3DCWebGui): 240 | return 'web' 241 | else: 242 | return None 243 | 244 | @property 245 | def gui(self): 246 | if self._try_connect: 247 | while self._try_connect: 248 | time.sleep(0.1) 249 | 250 | if self.connected: 251 | return self._gui 252 | 253 | self._try_connect = True 254 | 255 | def test_connection(testgui): 256 | requests = [] 257 | requests.append(RSCPTag.INFO_REQ_SERIAL_NUMBER) 258 | requests.append(RSCPTag.INFO_REQ_IP_ADDRESS) 259 | return testgui.get_data(requests, True) 260 | 261 | if self.cfgLoginusername and self.cfgLoginpassword and self.cfgLoginconnectiontype == 'auto': 262 | logger.info("Ermittle beste Verbindungsart (Verbindungsart auto)") 263 | seriennummer = self.cfgLoginseriennummer 264 | address = self.cfgLoginaddress 265 | testgui = None 266 | testgui_web = None 267 | if self.cfgLoginusername and self.cfgLoginpassword and not seriennummer: 268 | if self.cfgLoginusername and self.cfgLoginpassword and address and self.cfgLoginrscppassword: 269 | try: 270 | testgui = E3DCGui(self.cfgLoginusername, self.cfgLoginpassword, address, 271 | self.cfgLoginrscppassword) 272 | seriennummer = repr(test_connection(testgui)['INFO_SERIAL_NUMBER']) 273 | except: 274 | pass 275 | 276 | if not seriennummer: 277 | ret = self.getSerialnoFromWeb(self.cfgLoginusername, self.cfgLoginpassword) 278 | if len(ret) == 1: 279 | seriennummer = self.getSNFromNumbers(ret[0]['serialno']) 280 | 281 | if self.cfgLoginusername and self.cfgLoginpassword and self.cfgLoginrscppassword and seriennummer and not address and self.cfgLoginwebsocketaddr: 282 | logger.debug('Versuche IP-Adresse zu ermitteln') 283 | try: 284 | testgui = E3DCWebGui(self.cfgLoginusername, self.cfgLoginpassword, seriennummer) 285 | ip = repr(test_connection(testgui)['INFO_IP_ADDRESS']) 286 | if ip: 287 | address = ip 288 | logger.debug('IP-Adresse konnte ermittelt werden: ' + ip) 289 | testgui_web = testgui 290 | else: 291 | raise Exception('IP-Adresse konnte nicht ermittelt werden, kein Inahlt') 292 | except: 293 | testgui = None 294 | logger.exception('Bei der Ermittlung der IP-Adresse ist ein Fehler aufgetreten') 295 | 296 | if self.cfgLoginusername and self.cfgLoginpassword and address and self.cfgLoginrscppassword: 297 | logger.info('Teste direkte Verbindungsart') 298 | 299 | if not isinstance(testgui, E3DCGui): 300 | testgui = E3DCGui(self.cfgLoginusername, self.cfgLoginpassword, address, self.cfgLoginrscppassword) 301 | 302 | try: 303 | result = test_connection(testgui) 304 | if not seriennummer: 305 | seriennummer = repr(result['INFO_SERIAL_NUMBER']) 306 | logger.info('Verwende Direkte Verbindung / Verbindung mit System ' + repr( 307 | result['INFO_SERIAL_NUMBER']) + ' / ' + repr(result['INFO_IP_ADDRESS'])) 308 | 309 | self.serialnumber = result['INFO_SERIAL_NUMBER'] 310 | except ConnectionResetError as e: 311 | logger.warning( 312 | "Direkte Verbindung fehlgeschlagen (Socket) error({0}): {1}".format(e.errno, e.strerror)) 313 | testgui = None 314 | except RSCPCommunicationError as e: 315 | logger.warning("Direkte Verbindung fehlgeschlagen (RSCP)") 316 | testgui = None 317 | except: 318 | logger.exception('Fehler beim Aufbau der direkten Verbindung') 319 | testgui = None 320 | 321 | if self.cfgLoginusername and self.cfgLoginpassword and seriennummer and self.cfgLoginwebsocketaddr and not testgui: 322 | if testgui_web: 323 | logger.info('Verwende Web Verbindung') 324 | testgui = testgui_web 325 | else: 326 | logger.info('Teste Web Verbindungsart') 327 | testgui = E3DCWebGui(self.cfgLoginusername, self.cfgLoginpassword, seriennummer) 328 | try: 329 | result = test_connection(testgui) 330 | if not address: 331 | address = repr(result['INFO_IP_ADDRESS']) 332 | logger.info('Verwende Web Verbindung / Verbindung mit System ' + repr( 333 | result['INFO_SERIAL_NUMBER']) + ' / ' + repr(result['INFO_IP_ADDRESS'])) 334 | 335 | self.serialnumber = result['INFO_SERIAL_NUMBER'] 336 | except: 337 | logger.exception('Fehler beim Aufbau der Web Verbindung') 338 | testgui = None 339 | 340 | if not testgui: 341 | logger.error('Es konnte keine Verbindungsart ermittelt werden') 342 | else: 343 | if self.cfgLoginseriennummer != seriennummer: 344 | self.cfgLoginseriennummer = seriennummer 345 | self.txtConfigSeriennummer.SetValue(seriennummer) 346 | 347 | if self.cfgLoginaddress != address: 348 | self.cfgLoginaddress = address 349 | self.txtIP.SetValue(address) 350 | 351 | self._gui = testgui 352 | elif self.cfgLoginusername and self.cfgLoginpassword and self.cfgLoginaddress and self.cfgLoginrscppassword and self.cfgLoginconnectiontype == 'direkt': 353 | testgui = E3DCGui(self.cfgLoginusername, self.cfgLoginpassword, self.cfgLoginaddress, 354 | self.cfgLoginrscppassword) 355 | try: 356 | result = test_connection(testgui) 357 | self._gui = testgui 358 | logger.info('Verwende Direkte Verbindung') 359 | except: 360 | self._gui = None 361 | elif self.cfgLoginusername and self.cfgLoginpassword and self.cfgLoginseriennummer and self.cfgLoginwebsocketaddr and self.cfgLoginconnectiontype == 'web': 362 | testgui = E3DCWebGui(self.cfgLoginusername, self.cfgLoginpassword, self.cfgLoginseriennummer) 363 | try: 364 | result = test_connection(testgui) 365 | self._gui = testgui 366 | logger.info('Verwende Websocket') 367 | except: 368 | self._gui = None 369 | else: 370 | self._gui = None 371 | 372 | if not self._gui: 373 | logger.info('Kein Verbindungstyp kann verwendet werden, es fehlen Verbindungsdaten') 374 | 375 | self._try_connect = False 376 | return self._gui 377 | 378 | def getSNFromNumbers(self, sn): 379 | if sn[0:2] in ('70', '75'): 380 | return 'P10-' + sn 381 | elif sn[0:2] == '60': 382 | return 'Q10-' + sn 383 | elif sn[0:2] in ('85', '81', '82'): 384 | return 'H20-' + sn 385 | else: 386 | return 'S10-' + sn 387 | 388 | def getModelFromSerial(self, sn): 389 | sn = sn[4:] 390 | if sn[0:1] == '4' or sn[0:2] == '72': 391 | return "S10E" 392 | if sn[0:2] == '74': 393 | return "S10E Compact" 394 | if sn[0:1] == '5': 395 | return "S10 Mini" 396 | if sn[0:1] == '6': 397 | return "Quattroporte" 398 | if sn[0:2] == '70': 399 | return "S10E Pro" 400 | if sn[0:2] == '75': 401 | return "S10E Pro Compact" 402 | if sn[0:1] == '8': 403 | return "S10X" 404 | return "Unknown" 405 | 406 | def getSerialnoFromWeb(self, username, password): 407 | logger.debug('Ermittle Seriennummer über Webzugriff') 408 | userlevel = None 409 | 410 | try: 411 | r = requests.post('https://s10.e3dc.com/s10/phpcmd/cmd.php', data={'DO': 'LOGIN', 412 | 'USERNAME': username, 413 | 'PASSWD': hashlib.md5(password.encode()).hexdigest(), 414 | 'DENV': 'E3DC'}) 415 | r.raise_for_status() 416 | r_json = r.json() 417 | if r_json['ERRNO'] != 0: 418 | raise Exception('Abfrage Fehlerhaft #1, Fehlernummer ' + str(r_json['ERRNO'])) 419 | userlevel = int(r_json['CONTENT']['USERLEVEL']) 420 | cookies = r.cookies 421 | if userlevel in (1, 128): 422 | r = requests.post('https://s10.e3dc.com/s10/phpcmd/cmd.php', data={'DO': 'GETCONTENT', 423 | 'MODID': 'IDOVERVIEWCOMMONTABLE', 424 | 'ARG0': 'undefined', 425 | 'TOS': -7200, 426 | 'DENV': 'E3DC'}, cookies=cookies) 427 | r.raise_for_status() 428 | r_json = r.json() 429 | 430 | if r_json['ERRNO'] != 0: 431 | raise Exception('Abfrage fehlerhaft #2, Fehlernummer ' + str(r_json['ERRNO'])) 432 | 433 | content = r_json['CONTENT'] 434 | html = None 435 | for lst in content: 436 | if 'HTML' in lst: 437 | html = lst['HTML'] 438 | break 439 | 440 | if not html: 441 | raise Exception('Abfrage Fehlerhaft #3, Daten nicht gefunden') 442 | 443 | regex = r"s10list = '(\[\{.*\}\])';" 444 | 445 | try: 446 | match = re.search(regex, html, re.MULTILINE).group(1) 447 | obj = json.loads(match) 448 | return obj 449 | except: 450 | raise Exception('Abfrage Fehlerhaft #4, Regex nicht erfolgreich') 451 | 452 | except: 453 | logger.exception('Fehler beim Abruf der Seriennummer, Zugangsdaten fehlerhaft?') 454 | 455 | return [] 456 | 457 | def updateData(self): 458 | if not self._updateRunning: 459 | self._updateRunning = True 460 | try: 461 | if self.gui: 462 | logger.info('Aktualisiere Daten') 463 | self.clear_values() 464 | 465 | try: 466 | self.fill_info() 467 | except: 468 | logger.exception('Fehler beim Abruf der INFO-Daten') 469 | 470 | try: 471 | self.fill_bat() 472 | except: 473 | logger.exception('Fehler beim Abruf der BAT-Daten') 474 | 475 | try: 476 | self.fill_dcdc() 477 | except: 478 | logger.exception('Fehler beim Abruf der DCDC-Daten') 479 | 480 | try: 481 | self.fill_pvi() 482 | except: 483 | logger.exception('Fehler beim Abruf der PVI-Daten') 484 | 485 | try: 486 | self.fill_ems() 487 | self.fill_mbs() 488 | except: 489 | logger.exception('Fehler beim Abruf der EMS-Daten') 490 | 491 | try: 492 | self.fill_pm() 493 | except: 494 | logger.exception('Fehler beim Abruf der PM-Daten') 495 | 496 | try: 497 | self.fill_wb() 498 | except: 499 | logger.exception('Fehler beim Abruf der WB-Daten') 500 | 501 | try: 502 | self.fill_mbs() 503 | except: 504 | logger.exception('Fehler beim Abruf der Modbus-Daten') 505 | 506 | else: 507 | logger.warning('Konfiguration unvollständig, Verbindung nicht möglich') 508 | 509 | except: 510 | logger.exception('Fehler beim Aktualisieren der Daten') 511 | self._updateRunning = False 512 | 513 | 514 | def sammle_data(self, anon = True): 515 | logger.info('Sammle Daten') 516 | self.updateData() 517 | 518 | anonymize = ['DCDC_SERIAL_NUMBER', 'INFO_MAC_ADDRESS', 'BAT_DCB_SERIALNO', 'BAT_DCB_SERIALCODE', 'INFO_SERIAL_NUMBER', 519 | 'INFO_A35_SERIAL_NUMBER', 'PVI_SERIAL_NUMBER', 'INFO_PRODUCTION_DATE'] 520 | remove = ['INFO_IP_ADDRESS'] 521 | data = {} 522 | if self._data_bat: 523 | data['BAT_DATA'] = [] 524 | for d in self._data_bat: 525 | data['BAT_DATA'].append(d.asDict()) 526 | 527 | if self._data_dcdc: 528 | data['DCDC_DATA'] = [] 529 | for d in self._data_dcdc: 530 | data['DCDC_DATA'].append(d.asDict()) 531 | 532 | if self._data_ems: 533 | data['EMS_DATA'] = self._data_ems.asDict() 534 | 535 | if self._data_info: 536 | data['INFO_DATA'] = self._data_info.asDict() 537 | 538 | if self._data_pvi: 539 | data['PVI_DATA'] = {} 540 | for k in self._data_pvi: 541 | d = self._data_pvi[k] 542 | data['PVI_DATA'][k] = d.asDict() 543 | 544 | if self._data_pm: 545 | data['PM_DATA'] = {} 546 | for k in self._data_pm: 547 | d = self._data_pm[k] 548 | data['PM_DATA'][k] = d.asDict() 549 | 550 | if self._data_wb: 551 | data['WB_DATA'] = [] 552 | for d in self._data_wb: 553 | data['WB_DATA'].append(d.asDict()) 554 | if anon: 555 | logger.info('Anonymisiere Daten') 556 | data = self.anonymize_data(data, anonymize, remove) 557 | logger.info('Daten wurden anonymisiert') 558 | logger.info('Datensammlung beendet') 559 | return data 560 | 561 | def anonymize_data(self, data, anonymize, remove): 562 | if isinstance(data, dict): 563 | toremove = [] 564 | for i in data.keys(): 565 | if isinstance(data[i], dict) or isinstance(data[i], list): 566 | data[i] = self.anonymize_data(data[i], anonymize, remove) 567 | elif i in anonymize: 568 | if isinstance(data[i], int) or isinstance(data[i], float): 569 | data[i] = str(data[i]) 570 | if len(data[i]) >= 6: 571 | tmp = 'X' * (len(data[i])-6) 572 | data[i] = data[i][:6] + tmp 573 | else: 574 | data[i] = 'X' * len(data[i]) 575 | elif i in remove: 576 | toremove.append(i) 577 | 578 | for r in toremove: 579 | del data[r] 580 | elif isinstance(data, list): 581 | nl = [] 582 | for i in data: 583 | nl += [self.anonymize_data(i, anonymize, remove)] 584 | data = nl 585 | return data 586 | 587 | def setMaxChargePower(self, value): 588 | temp = [RSCPDTO(tag=RSCPTag.EMS_MAX_CHARGE_POWER, rscp_type=RSCPType.Uint32, data=int(value))] 589 | r = [RSCPDTO(tag=RSCPTag.EMS_REQ_SET_POWER_SETTINGS, rscp_type=RSCPType.Container, data=temp)] 590 | 591 | res = self.gui.get_data(r, True) 592 | logger.info('Wert über setMaxChargePower auf ' + str(value) + ' geändert') 593 | 594 | def setMaxDischargePower(self, value): 595 | temp = [RSCPDTO(tag=RSCPTag.EMS_MAX_DISCHARGE_POWER, rscp_type=RSCPType.Uint32, data=int(value))] 596 | r = [RSCPDTO(tag=RSCPTag.EMS_REQ_SET_POWER_SETTINGS, rscp_type=RSCPType.Container, data=temp)] 597 | 598 | res = self.gui.get_data(r, True) 599 | logger.info('Wert über setMaxDischargePower auf ' + str(value) + ' geändert') \ 600 | 601 | def setDischargeStartPower(self, value): 602 | temp = [RSCPDTO(tag=RSCPTag.EMS_DISCHARGE_START_POWER, rscp_type=RSCPType.Uint32, data=int(value))] 603 | r = [RSCPDTO(tag=RSCPTag.EMS_REQ_SET_POWER_SETTINGS, rscp_type=RSCPType.Container, data=temp)] 604 | 605 | res = self.gui.get_data(r, True) 606 | logger.info('Wert über setDischageStartPower auf ' + str(value) + ' geändert') \ 607 | 608 | @property 609 | def mqttclient(self): 610 | #TODO: Weitere Möglichkeiten ergänzen 611 | sublist = { 612 | 'E3DC/EMS_DATA/EMS_GET_POWER_SETTINGS/EMS_MAX_CHARGE_POWER': self.setMaxChargePower, 613 | 'E3DC/EMS_DATA/EMS_GET_POWER_SETTINGS/EMS_MAX_DISCHARGE_POWER': self.setMaxDischargePower, 614 | 'E3DC/EMS_DATA/EMS_GET_POWER_SETTINGS/EMS_DISCHARGE_START_POWER': self.setDischargeStartPower 615 | 616 | } 617 | 618 | def on_message(client, userdata, message): 619 | topic = message.topic[1:] 620 | if topic[-4:] == '/SET': 621 | topic = topic[:-4] 622 | if topic in self.cfgExportpathnames.values(): 623 | path = list(self.cfgExportpathnames.keys())[list(self.cfgExportpathnames.values()).index(topic)] 624 | if path in sublist.keys(): 625 | callback = sublist[path] 626 | value = str(message.payload.decode("utf-8")) 627 | try: 628 | test = int(self._exportcache[topic]) 629 | if test != value: 630 | callback(value) 631 | else: 632 | logger.debug(topic + ' Wert hat sich nicht geändert ' + str(value) + ' <-> ' + str(test)) 633 | except: 634 | logger.exception('Fehler bei ' + topic + ' (' + value + ')') 635 | 636 | def on_connect(client, userdata, flags, rc): 637 | logger.debug('MQTT-Client connected') 638 | if sub: 639 | for path in sublist.keys(): 640 | if path in self.cfgExportpathnames.keys(): 641 | topic = '/' + self.cfgExportpathnames[path] + '/SET' 642 | logger.debug('MQTT Subscribe: ' + topic) 643 | self._mqttclient.subscribe(topic) 644 | 645 | def on_disconnect(client, userdata, rc): 646 | logger.debug('MQTT-Client disconnect') 647 | 648 | if self._mqttclient is not None: 649 | if self._mqttclient.is_connected(): 650 | return self._mqttclient 651 | 652 | self._mqttclient = None 653 | 654 | if self.cfgExportmqtt if 'paho' in sys.modules.keys() else False: 655 | broker = self.cfgExportmqttbroker 656 | port = self.cfgExportmqttport 657 | sub = self.cfgExportmqttsub 658 | username = self.cfgExportmqttusername 659 | password = self.cfgExportmqttpassword 660 | zertifikat = self.cfgExportmqttzertifikat 661 | insecure = self.cfgExportmqttinsecure 662 | 663 | logger.info('Verbinde mit MQTT-Broker ' + broker + ':' + str(port)) 664 | 665 | self._mqttclient = paho.Client("RSCPGui") 666 | 667 | if username and password: 668 | self._mqttclient.username_pw_set(username, password) 669 | 670 | if insecure and zertifikat: 671 | if os.path.isfile(zertifikat): 672 | self._mqttclient.tls_set(ca_certs=zertifikat) 673 | self._mqttclient.tls_insecure_set(insecure) 674 | 675 | self._mqttclient.enable_logger(logger) 676 | 677 | self._mqttclient.on_message=on_message 678 | self._mqttclient.on_disconnect=on_disconnect 679 | self._mqttclient.on_connect=on_connect 680 | self._mqttclient.connect(broker, port) 681 | self._mqttclient.loop_start() 682 | 683 | return self._mqttclient 684 | 685 | def StartAutoExport(self): 686 | 687 | def influx_connect(influxhost, influxport, influxtimeout, influxdatenbank): 688 | logger.debug('Verbinde mit Influxdb ' + influxhost + ':' + str(influxport) + '/' + influxdatenbank) 689 | influxclient = influxdb.InfluxDBClient(host=influxhost, port=influxport, timeout=influxtimeout) 690 | influxclient.switch_database(influxdatenbank) 691 | 692 | return influxclient 693 | 694 | try: 695 | logger.info('Starte automatischen Export') 696 | if len(self.cfgExportpaths) > 0: 697 | logger.debug('Es sind ' + str(len(self.cfgExportpaths)) + ' Datenfelder zum Export vorgesehen') 698 | else: 699 | logger.debug('Es wurden keine Exporfelder definiert!') 700 | 701 | csvwriter = None 702 | csvfile = None 703 | 704 | csvactive = self.cfgExportcsv 705 | csvfilename = self.cfgExportcsvfile 706 | 707 | jsonactive = self.cfgExportjson 708 | jsonfilename = self.cfgExportjsonfile 709 | 710 | mqttqos = self.cfgExportmqttqos 711 | mqttretain = self.cfgExportmqttretain 712 | 713 | httpactive = self.cfgExporthttp 714 | httpurl = self.cfgExporthttpurl 715 | 716 | influxactive = self.cfgExportinflux if 'influxdb' in sys.modules.keys() else False 717 | influxhost = self.cfgExportinfluxhost 718 | influxport = self.cfgExportinfluxport 719 | influxdatenbank = self.cfgExportinfluxdatenbank 720 | influxtimeout = self.cfgExportinfluxtimeout 721 | influxname = self.cfgExportinfluxname 722 | influxclient = influx_connect(influxhost, influxport, influxtimeout, influxdatenbank) if influxactive else None 723 | 724 | intervall = self.cfgExportintervall 725 | 726 | notificationactive = False 727 | if 'telegram' in sys.modules.keys(): 728 | if self.cfgNotificationtelegram is True: 729 | if not self.cfgNotificationtelegramtoken or not self.cfgNotificationtelegramempfaenger: 730 | logger.warning('Benachrichtigung an Telegram nicht möglich, Token oder Empfänger sind nicht gefüllt') 731 | else: 732 | self.notificationblocker = {} 733 | notificationactive = True 734 | 735 | if csvactive: 736 | csvfile = open(csvfilename, 'a', newline='') 737 | fields: list = self.cfgExportpaths.copy() 738 | 739 | for key,val in enumerate(fields): 740 | if val in self.cfgExportpathnames: 741 | fields[key] = self.cfgExportpathnames[val] 742 | 743 | fields.insert(0,'datetime') 744 | fields.insert(0,'ts') 745 | csvwriter = csv.DictWriter(csvfile, fieldnames = fields) 746 | csvwriter.writeheader() 747 | 748 | while self._AutoExportStarted: 749 | laststart = time.time() 750 | logger.debug('Exportiere Daten (autoexport)') 751 | try: 752 | values, value_path = self.getUploadDataFromPath() 753 | values['ts'] = time.time() 754 | values['datetime'] = datetime.datetime.now().isoformat() 755 | self._exportcache = values 756 | if csvactive: 757 | threading.Thread(target=self.exportCSV, args=(csvfilename, csvwriter, csvfile, values)).start() 758 | 759 | if jsonactive: 760 | threading.Thread(target=self.exportJson, args=(jsonfilename, values)).start() 761 | 762 | if self.mqttclient is not None: 763 | threading.Thread(target=self.exportMQTT, args=(mqttqos, mqttretain, values)).start() 764 | 765 | if httpactive: 766 | threading.Thread(target=self.exportHTTP, args=(httpurl, values)).start() 767 | 768 | if influxactive: 769 | threading.Thread(target=self.exportInflux, args=(influxclient, influxname,values)).start() 770 | 771 | if notificationactive: 772 | self.notify(value_path) 773 | #threading.Thread(target=self.notify, args={values}).start() 774 | except: 775 | logger.exception('Fehler beim Abruf der Exportdaten') 776 | 777 | diff = time.time() - laststart 778 | if diff < intervall: 779 | wait = intervall - diff 780 | logger.debug('Warte ' + str(wait) + 's') 781 | waits = int(math.floor(wait)) 782 | time.sleep(wait-waits) 783 | for i in range(0,waits): 784 | if self._AutoExportStarted: 785 | time.sleep(1) 786 | else: 787 | break 788 | 789 | if self._mqttclient is not None: 790 | self._mqttclient.loop_stop() 791 | self._mqttclient.disconnect() 792 | 793 | if csvactive: 794 | csvfile.close() 795 | 796 | 797 | except: 798 | logger.exception('Fehler beim automatischen Export') 799 | 800 | self._AutoExportStarted = False 801 | 802 | def exportCSV(self, csvfilename, csvwriter, csvfile, values): 803 | logger.info('Exportiere in CSV-Datei ' + csvfilename) 804 | try: 805 | csvwriter.writerow(values) 806 | csvfile.flush() 807 | logger.debug('Export in CSV-Datei erfolgreich') 808 | except: 809 | logger.exception('Fehler beim Export in CSV-Datei') 810 | 811 | def exportHTTP(self, httpurl, values): 812 | try: 813 | logger.info('Exportiere an Http-Url ' + httpurl) 814 | r = requests.post(httpurl, json=values) 815 | r.raise_for_status() 816 | logger.debug('Export an URL erfolgreich ' + str(r.status_code)) 817 | logger.debug('Response: ' + r.text) 818 | except: 819 | logger.exception('Fehler beim Export in Http') 820 | 821 | def exportMQTT(self, mqttqos, mqttretain, values): 822 | try: 823 | logger.info('Exportiere nach MQTT') 824 | for key in values.keys(): 825 | if key not in ('ts', 'datetime'): 826 | topic = '/' + key 827 | res, mid = self.mqttclient.publish(topic, values[key], mqttqos, mqttretain) 828 | if res != 0: 829 | self.mqttclient.disconnect() 830 | 831 | logger.debug('Export an MQTT abgeschlossen') 832 | except: 833 | logger.exception('Fehler beim Export nach MQTT') 834 | 835 | def exportJson(self, jsonfilename, values): 836 | try: 837 | logger.info('Exportiere in JSON-Datei ' + jsonfilename) 838 | with open(jsonfilename, 'w') as jsonfile: 839 | json.dump(values, jsonfile) 840 | logger.debug('Export an JSON-Datei erfolgreich') 841 | except: 842 | logger.exception('Fehler beim Export in JSON-Datei') 843 | 844 | def exportInflux(self, influxclient, influxname, fields): 845 | try: 846 | logger.info('Exportiere an Influxdb') 847 | values = {} 848 | for key in fields.keys(): 849 | if key not in ('ts', 'datetime'): 850 | values[key] = fields[key] 851 | 852 | if len(values) > 0: 853 | write_points = [{'measurement': influxname, 'fields': values}] 854 | influxclient.write_points(write_points) 855 | 856 | logger.debug('Export an Influxdb erfolgreich') 857 | else: 858 | logger.warning('Keine Daten zur Übergabe an Influxdb zur Verfügung') 859 | except: 860 | logger.exception('Fehler beim Export an Influxdb') 861 | 862 | def notify(self, values): 863 | # path|datatype|expression|notificationservice|text|waittime(seconds) 864 | #[Notification / Rules] 865 | #1 = E3DC / EMS_DATA / EMS_POWER_GRID | int | {value} < -2000 | telegram | Einspeiseleistung > 2000 866 | #W({value}) | 3600 867 | 868 | 869 | def execute_rule(value, datatype, expression, text): 870 | try: 871 | if datatype in ('int', 'integer', 'smallint', 'uint', 'int16', 'int32', 'int64'): 872 | value = int(value) 873 | elif datatype in ('float', 'numeric', 'double'): 874 | value = float(value) 875 | elif datatype in ('str','string',''): 876 | value = str(value) 877 | except: 878 | logger.exception('Datenkonvertierung in notify fehlgeschlagen: ' + str(value) + ' Datentyp: ' + datatype) 879 | 880 | expression = expression.format(value=str(value)) 881 | logger.debug('Validiere Daten ' + expression ) 882 | try: 883 | if eval(expression): 884 | text = text.format(value=str(value)) 885 | 886 | return text 887 | except: 888 | logger.exception('Fehler beim Ausführen der Expression: ' + expression) 889 | 890 | return '' 891 | 892 | def send_telegram(text): 893 | logger.debug('Sende Nachricht über Telegram: ' + text) 894 | try: 895 | bot = telegram.Bot(token=self.cfgNotificationtelegramtoken) 896 | bot.send_message(chat_id=self.cfgNotificationtelegramempfaenger, text=text) 897 | except: 898 | logger.exception('Fehler beim versand einer Telegram-Benachrichtigung') 899 | 900 | logger.debug('Sende Benachrichtigungen') 901 | rules = self._config['Notification/Rules'] 902 | for rule in rules: 903 | path,datatype,expression,service,text,waittime = rules[rule].split('|') 904 | waittime = float(waittime) 905 | telegramactive = service == 'telegram' 906 | 907 | block = False 908 | if waittime > 0: 909 | if rule in self._notificationblocker: 910 | if time.time() - self._notificationblocker[rule] < waittime: 911 | logger.debug('Benachrichtigung nicht verschickt, Wartezeit nicht abgelaufen ' + str(time.time() - self._notificationblocker[rule]) + 'ts < ' + str(waittime)) 912 | block = True 913 | 914 | if not block: 915 | if path in values: 916 | if values[path] is not None: 917 | logger.debug('Regel (' + rule + ') für Path ' + path + ' gefunden mit Value: ' + str(values[path])) 918 | if waittime == -1: 919 | if rule not in self._notificationblocker: 920 | pass 921 | elif self._notificationblocker[rule] != values[path]: 922 | text = execute_rule(values[path], datatype, expression, text) 923 | if text != '': 924 | if telegramactive: 925 | send_telegram(text) 926 | else: 927 | logger.debug('Keine Benachrichtigung für ' + path + ' verschickt, Wert hat sich nicht geändert ' + str(values[path])) 928 | 929 | self._notificationblocker[rule] = values[path] 930 | 931 | else: 932 | text = execute_rule(values[path], datatype, expression, text) 933 | if text != '': 934 | self._notificationblocker[rule] = time.time() 935 | if telegramactive: 936 | send_telegram(text) 937 | else: 938 | logger.warning('Wert für Pfad ' + path + ' konnte nicht ermittelt werden, überspringe Benachrichtigung') 939 | 940 | 941 | def getUploadDataFromPath(self): 942 | def getDataFromPath(teile, data): 943 | if data is not None: 944 | if isinstance(data, dict): 945 | if teile[0] in data.keys(): 946 | if len(teile) == 1: 947 | return data[teile[0]] 948 | else: 949 | return getDataFromPath(teile[1:], data[teile[0]]) 950 | elif isinstance(data, list): 951 | if data[int(teile[0])] is not None: 952 | if len(teile) == 1: 953 | return data[int(teile[0])] 954 | else: 955 | return getDataFromPath(teile[1:], data[int(teile[0])]) 956 | else: 957 | logger.warning('Element not Found ' + '/'.join(teile)) 958 | 959 | ems_data = None 960 | bat_data = {} 961 | info_data = None 962 | dcdc_data = {} 963 | pm_data = {} 964 | pvi_data = {} 965 | wb_data = {} 966 | 967 | values = {} 968 | value_path = {} 969 | 970 | for path in self.cfgExportpaths: 971 | logger.debug('Ermittle Pfad aus ' + path) 972 | teile = path.split('/') 973 | if teile[0] == 'E3DC': 974 | newvalue = None 975 | if teile[1] == 'EMS_DATA': 976 | try: 977 | if not ems_data: 978 | ems_data = self._fill_ems().asDict() 979 | 980 | newvalue = getDataFromPath(teile[2:], ems_data) 981 | except: 982 | logger.exception('Fehler beim Abruf von EMS') 983 | elif teile[1] == 'BAT_DATA': 984 | try: 985 | index = int(teile[2]) 986 | if index not in bat_data.keys(): 987 | bat_data[index] = self.gui.get_data(self.gui.getBatDcbData(bat_index=index), True).asDict() 988 | newvalue = getDataFromPath(teile[3:], bat_data[index]) 989 | except: 990 | logger.exception('Fehler beim Abruf von BAT') 991 | elif teile[1] == 'INFO_DATA': 992 | try: 993 | if not info_data: 994 | info_data = self._fill_info().asDict() 995 | newvalue = getDataFromPath(teile[2:], info_data) 996 | except: 997 | logger.exception('Fehler beim Abruf von INFO') 998 | elif teile[1] == 'DCDC_DATA': 999 | try: 1000 | index = int(teile[2]) 1001 | if index not in dcdc_data.keys(): 1002 | dcdc_data[index] = self.gui.get_data(self.gui.getDCDCData(dcdc_indexes=index), True).asDict() 1003 | newvalue = getDataFromPath(teile[3:], dcdc_data[index]) 1004 | except: 1005 | logger.exception('Fehler beim Abruf von DCDC') 1006 | elif teile[1] == 'PM_DATA': 1007 | try: 1008 | index = int(teile[2]) 1009 | if index not in pm_data.keys(): 1010 | pm_data[index] = self.gui.get_data(self.gui.getPMData(pm_index=index), True).asDict() 1011 | newvalue = getDataFromPath(teile[3:], pm_data[index]) 1012 | except: 1013 | logger.exception('Fehler beim Abruf von PM') 1014 | elif teile[1] == 'PVI_DATA': 1015 | try: 1016 | index = int(teile[2]) 1017 | if index not in pvi_data.keys(): 1018 | pvi_data[index] = self.gui.get_data(self.gui.getPVIData(pvi_index=index), True).asDict() 1019 | newvalue = getDataFromPath(teile[3:], pvi_data[index]) 1020 | except: 1021 | logger.exception('Fehler beim Abruf von PVI') 1022 | elif teile[1] == 'WB_DATA': 1023 | try: 1024 | index = int(teile[2]) 1025 | if index not in wb_data.keys(): 1026 | wb_data[index] = self.gui.get_data(self.gui.getWB(index=index), True).asDict() 1027 | newvalue = getDataFromPath(teile[3:], wb_data[index]) 1028 | except: 1029 | logger.exception('Fehler beim Abruf von WB') 1030 | 1031 | if path in self.cfgExportpathnames: 1032 | key = self.cfgExportpathnames[path] 1033 | if key == '': 1034 | key = path 1035 | else: 1036 | key = path 1037 | value_path[path] = newvalue 1038 | values[key] = newvalue 1039 | else: 1040 | logger.debug('Pfadangabe falsch: ' + path) 1041 | 1042 | return values, value_path 1043 | 1044 | def _fill_info(self): 1045 | logger.debug('Rufe INFO-Daten ab') 1046 | 1047 | requests = self.gui.getInfo() + self.gui.getUpdateStatus() + self.gui.getInfoAdditional() 1048 | data = self.gui.get_data(requests, True, waittime=0.1) 1049 | rscp_data = self.gui.get_data(self.gui.getUPNPData(), True) 1050 | 1051 | logger.debug('Abruf INFO-Daten abgeschlossen') 1052 | return data 1053 | 1054 | def fill_info(self): 1055 | self._data_info = self._fill_info() 1056 | 1057 | def _fill_ems(self): 1058 | logger.debug('Rufe EMS-Daten ab') 1059 | self._extsrcavailable = 0 1060 | data = self.gui.get_data(self.gui.getEMSData(), True) 1061 | logger.debug('Abruf EMS-Daten abgeschlossen') 1062 | return data 1063 | 1064 | def fill_ems(self): 1065 | self._data_ems = self._fill_ems() 1066 | 1067 | def _fill_mbs(self): 1068 | logger.debug('Rufe Modbus-Daten ab') 1069 | data = self.gui.get_data(self.gui.getModbus(), True) 1070 | logger.debug('Abruf Modbus-Daten abgeschlossen') 1071 | return data 1072 | 1073 | def fill_mbs(self): 1074 | self._data_mbs = self._fill_mbs() 1075 | 1076 | def _fill_dcdc(self): 1077 | logger.debug('Rufe DCDC-Daten ab') 1078 | data = [] 1079 | if len(self._dcdc_available) > 0: 1080 | indexes = self._dcdc_available 1081 | else: 1082 | indexes = [0,1,2,3] 1083 | for index in indexes: 1084 | try: 1085 | d = self.gui.get_data(self.gui.getDCDCData(dcdc_indexes=[index]), True) 1086 | index = int(d['DCDC_INDEX']) 1087 | data.append(d) 1088 | 1089 | logger.info('DCDC #' + str(index) + ' wurde erfolgreich abgefragt.') 1090 | if index not in self._dcdc_available: 1091 | self._dcdc_available.append(index) 1092 | except: 1093 | logger.debug('DCDC #' + str(index) + ' konnte nicht abgefragt werden.') 1094 | 1095 | return data 1096 | 1097 | def fill_dcdc(self): 1098 | self._data_dcdc = self._fill_dcdc() 1099 | 1100 | def _fill_pvi(self): 1101 | logger.debug('Rufe PVI-Daten ab') 1102 | data = {} 1103 | if len(self._pvi_available) > 0: 1104 | indexes = self._pvi_available 1105 | else: 1106 | indexes = [0, 1, 2, 3] 1107 | for index in indexes: 1108 | try: 1109 | rscp_data = self.gui.get_data(self.gui.getPVIData(pvi_index=index), True) 1110 | # Fix Issue #33 1111 | data[index] = rscp_data['PVI_DATA'] if 'PVI_DATA' in rscp_data else rscp_data 1112 | logger.info('PVI #' + str(index) + ' wurde erfolgreich abgefragt.') 1113 | if index not in self._pvi_available: 1114 | self._pvi_available.append(index) 1115 | except: 1116 | logger.debug('PVI #' + str(index) + ' konnte nicht abgefragt werden.') 1117 | 1118 | logger.debug('Abruf PVI-Daten abgeschlossen') 1119 | 1120 | return data 1121 | 1122 | def fill_pvi(self): 1123 | self._data_pvi = self._fill_pvi() 1124 | 1125 | def _fill_pm(self): 1126 | logger.debug('Rufe PM-Daten ab') 1127 | data = {} 1128 | 1129 | if len(self._pm_available) > 0: 1130 | indexes = self._pm_available 1131 | else: 1132 | indexes = range(0,8) 1133 | 1134 | for index in indexes: 1135 | try: 1136 | d = self.gui.get_data(self.gui.getPMData(pm_index=index), True) 1137 | 1138 | if 'PM_DEVICE_STATE' not in d or d['PM_DEVICE_STATE'].type != RSCPType.Error: 1139 | index = d['PM_INDEX'].data 1140 | data[index] = d 1141 | logger.info('PM #' + str(index) + ' erfolgreich abgerufen') 1142 | if index not in self._pm_available: 1143 | self._pm_available.append(index) 1144 | except: 1145 | logger.exception('PM #' + str(index) + ' konnte nicht abgerufen werden.') 1146 | 1147 | logger.debug('Abruf PM-Daten abgeschlossen') 1148 | 1149 | return data 1150 | 1151 | def fill_pm(self): 1152 | self._data_pm = self._fill_pm() 1153 | 1154 | def _fill_bat(self): 1155 | logger.debug('Rufe BAT-Daten ab') 1156 | data = [] 1157 | for index in [0,1]: 1158 | try: 1159 | requests = self.gui.getBatDcbData(bat_index=index) 1160 | if len(requests) > 0: 1161 | f = self.gui.get_data(requests, True) 1162 | data.append(f) 1163 | logger.info('Erfolgreich BAT #' + str(index) + ' abgerufen') 1164 | except: 1165 | logger.debug('Fehler beim Abruf von BAT #' + str(index)) 1166 | 1167 | logger.debug('BAT-Daten abgerufen') 1168 | 1169 | return data 1170 | 1171 | def fill_bat(self): 1172 | self._data_bat = self._fill_bat() 1173 | 1174 | def _fill_wb(self): 1175 | ddata = [] 1176 | logger.debug('Rufe WB-Daten ab') 1177 | try: 1178 | data = self.gui.get_data(self.gui.getWBCount(), True) 1179 | if data.type == RSCPType.Error: 1180 | raise RSCPCommunicationError('Error bei WB-Abruf', logger, data) 1181 | except RSCPCommunicationError: 1182 | logger.info('Keine Wallbox vorhanden') 1183 | return ddata 1184 | 1185 | for index in data: 1186 | logger.debug('Rufe Daten für Wallbox #' + str(index) + ' ab') 1187 | d = self.gui.get_data(self.gui.getWB(index=index), True) 1188 | 1189 | ddata.append(d) 1190 | 1191 | logger.debug('Abruf WB-Daten abgeschlossen') 1192 | 1193 | return ddata 1194 | 1195 | def fill_wb(self): 1196 | self._data_wb = self._fill_wb() 1197 | 1198 | def deleteFromPortal(self): 1199 | logger.info('Lösche Daten aus Portal') 1200 | try: 1201 | data = {} 1202 | if not self._data_info: 1203 | self._data_info = self._fill_info() 1204 | if not self._data_info: 1205 | raise Exception('Abruf nicht möglich') 1206 | 1207 | sn = self._data_info['INFO_SERIAL_NUMBER'].data 1208 | mac = self._data_info['INFO_MAC_ADDRESS'].data 1209 | snmac = sn+mac 1210 | system = hashlib.md5(snmac.encode()).hexdigest() 1211 | 1212 | url = 'https://pv.pincrushers.de/rscpgui/' + system 1213 | logger.debug('Lösche Portaldaten mit URL ' + url) 1214 | 1215 | r = requests.delete(url) 1216 | 1217 | logger.debug('Http Status Code: ' + str(r.status_code)) 1218 | logger.debug('Response: ' + r.text) 1219 | 1220 | response = r.json() 1221 | r.raise_for_status() 1222 | 1223 | logger.info('Daten erfolgreich aus Portal gelöscht') 1224 | return response 1225 | except: 1226 | logger.exception('Daten konnte nicht aus dem Portal gelöscht werden') 1227 | return None 1228 | 1229 | def sendToPortalMin(self): 1230 | logger.info('Sende Daten an Portal') 1231 | try: 1232 | data = {} 1233 | if not self._data_info: 1234 | self._data_info = self._fill_info() 1235 | if not self._data_info: 1236 | raise Exception('Abruf nicht möglich') 1237 | if not self._data_bat: 1238 | self._data_bat = self._fill_bat() 1239 | if not self._data_bat: 1240 | raise Exception('Abruf nicht möglich') 1241 | 1242 | sn = self._data_info['INFO_SERIAL_NUMBER'].data 1243 | mac = self._data_info['INFO_MAC_ADDRESS'].data 1244 | snmac = sn+mac 1245 | system = hashlib.md5(snmac.encode()).hexdigest() 1246 | proddate = self._data_info['INFO_PRODUCTION_DATE'].data 1247 | if len(proddate) == 16: 1248 | proddate = proddate[3:10] 1249 | data['productiondate'] = proddate 1250 | data['release'] = self._data_info['INFO_SW_RELEASE'].data 1251 | data['model'] = sn[0:6] 1252 | 1253 | data['bat'] = [] 1254 | 1255 | for bat in self._data_bat: 1256 | databat = {} 1257 | databat['capacity'] = bat['BAT_SPECIFICATION']['BAT_SPECIFIED_CAPACITY'].data 1258 | databat['dcb'] = [] 1259 | 1260 | dcbcount = int(bat['BAT_DCB_COUNT']) 1261 | dcbinfo = bat['BAT_DCB_INFO'] if dcbcount > 1 else [bat['BAT_DCB_INFO']] 1262 | 1263 | for dcb in dcbinfo: 1264 | datadcb = {} 1265 | try: 1266 | datadcb['cyclecount'] = dcb['BAT_DCB_CYCLE_COUNT'].data 1267 | except AttributeError: 1268 | datadcb['cyclecount'] = bat['BAT_CHARGE_CYCLES'].data 1269 | 1270 | try: 1271 | datadcb['soh'] = dcb['BAT_DCB_SOH'].data 1272 | except AttributeError: 1273 | datadcb['soh'] = bat['BAT_ASOC'].data 1274 | 1275 | try: 1276 | datadcb['maxchargevoltage'] = dcb['BAT_DCB_MAX_CHARGE_VOLTAGE'].data 1277 | except AttributeError: 1278 | datadcb['maxchargevoltage'] = bat['BAT_MAX_BAT_VOLTAGE'].data 1279 | 1280 | try: 1281 | datadcb['endofdischarge'] = dcb['BAT_DCB_END_OF_DISCHARGE'].data 1282 | except AttributeError: 1283 | datadcb['endofdischarge'] = bat['BAT_EOD_VOLTAGE'].data 1284 | 1285 | try: 1286 | datadcb['manufacture'] = dcb['BAT_DCB_MANUFACTURE_NAME'].data 1287 | except AttributeError: 1288 | #datadcb['manufacture'] = bat['BAT_'] 1289 | datadcb['manufacture'] = bat['BAT_DEVICE_NAME'].data 1290 | 1291 | try: 1292 | datadcb['type'] = dcb['BAT_DCB_DEVICE_NAME'].data 1293 | except AttributeError: 1294 | datadcb['type'] = bat['BAT_DCB_TYPE'].data 1295 | 1296 | databat['dcb'].append(datadcb) 1297 | 1298 | data['bat'].append(databat) 1299 | logger.debug('Daten: ' + json.dumps(data)) 1300 | 1301 | url = 'https://pv.pincrushers.de/rscpgui/' + system 1302 | logger.debug('Sende Portaldaten an url ' + url) 1303 | 1304 | r = requests.put(url, json = data) 1305 | 1306 | logger.debug('Http Status Code: ' + str(r.status_code)) 1307 | logger.debug('Response: ' + r.text) 1308 | 1309 | response = r.json() 1310 | r.raise_for_status() 1311 | 1312 | logger.info('Daten erfolgreich an Portal übermittelt') 1313 | return response 1314 | except: 1315 | logger.exception('Daten konnten nicht an das Portal übermittelt werden') 1316 | return None 1317 | 1318 | 1319 | 1320 | --------------------------------------------------------------------------------