├── .gitmodules ├── requirements.txt ├── db_postgrest_model.py ├── LICENSE ├── mqtt.py ├── smartmeter_cfg_vorlage.toml ├── .gitignore ├── smartmeter_telegrambot.py ├── setup_logging.py ├── README.md ├── smartmeter.py ├── db_model.py └── electric_meter.py /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "telegram_api"] 2 | path = telegram_api 3 | url = https://github.com/Hofei90/telegram_api.git 4 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | minimalmodbus~=2.0.1 2 | peewee~=3.13.1 3 | pyserial~=3.4 4 | requests~=2.31.0 5 | toml~=0.10.2 6 | systemd-python==234 7 | paho-mqtt~=1.6.1 8 | -------------------------------------------------------------------------------- /db_postgrest_model.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import json 3 | from copy import deepcopy 4 | 5 | import requests 6 | 7 | 8 | def sende_daten(url, headers, daten, none_daten, logger): 9 | for datensatz in daten: 10 | datensatz["ts"] = datensatz["ts"].strftime("%Y-%m-%d %H:%M:%S") 11 | daten_konvertiert = [] 12 | for datensatz in daten: 13 | datensatz_konvertiert = deepcopy(none_daten) 14 | for key, value in datensatz.items(): 15 | datensatz_konvertiert[key.lower()] = value 16 | daten_konvertiert.append(datensatz_konvertiert) 17 | 18 | json_daten = json.dumps(daten_konvertiert) 19 | logger.debug(json_daten) 20 | r = requests.post(url, headers=headers, data=json_daten) 21 | if r.status_code == 200 or r.status_code == 201: 22 | logger.debug(r.status_code) 23 | logger.debug(r.text) 24 | else: 25 | logger.error(r.status_code) 26 | logger.error(r.text) 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Hofei90 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 | -------------------------------------------------------------------------------- /mqtt.py: -------------------------------------------------------------------------------- 1 | import paho.mqtt.publish as publish 2 | 3 | 4 | class MQTT: 5 | # Wertinitialisierung von config 6 | def __init__(self, broker, port, topic, username, password): 7 | self.config_broker = broker 8 | self.config_port = port 9 | self.config_username = username 10 | self.config_password = password 11 | self.config_topic = topic 12 | 13 | # Wir schicken die daten als 14 | # 15 | def send(self, daten, logger): 16 | # Vorbereitung der daten für MQTT 17 | msgs = [] 18 | for i, (k, v) in enumerate(daten[0].items()): 19 | # wir brauchen keinen Zeitwert, weil alle Daten sind von jetzt 20 | if k != "ts": 21 | msgs.append({'topic': self.config_topic + k, 'payload': str(v)}) 22 | 23 | # Data senden mit einem einzigen MQTT Aufruf 24 | if self.config_username != "": 25 | auth_data = {'username': self.config_username, 'password': self.config_password} 26 | publish.multiple(msgs, hostname=self.config_broker, port=int(self.config_port), client_id="smartmeter", 27 | keepalive=60, auth=auth_data) 28 | else: 29 | publish.multiple(msgs, hostname=self.config_broker, port=int(self.config_port), client_id="smartmeter", 30 | keepalive=60) 31 | -------------------------------------------------------------------------------- /smartmeter_cfg_vorlage.toml: -------------------------------------------------------------------------------- 1 | [modbus] 2 | serial_if="/dev/ttyUSB0" 3 | serial_if_baud=19200 4 | serial_if_byte=8 5 | serial_if_par="E" 6 | serial_if_stop=1 7 | slave_addr=1 8 | timeout=0.6 9 | 10 | 11 | [db] 12 | db = "" # "postgresql" | "mysql" | "sqlite" | "postgrest" 13 | 14 | [db.postgresql] 15 | database = "" 16 | user = "" 17 | password = "" 18 | host = "" 19 | port = 0 20 | 21 | [db.mysql] 22 | database = "" 23 | user = "" 24 | password = "" 25 | host = "" 26 | port = 0 27 | 28 | [db.sqlite] 29 | database = ":memory:" # Pfad zu Datenbankdatei oder :memory: 30 | 31 | [db.postgrest] 32 | url = "" 33 | user = "" 34 | token = "" 35 | table = "" 36 | 37 | [mqtt] 38 | broker = "" 39 | port = "" 40 | username = "" # empty disables authentication 41 | password = "" # empty disables authentication 42 | topic = "" # empty defaults to /smartmeter/ 43 | is_active = false # aktiv wenn true, inaktiv wenn false 44 | 45 | [telegram_bot] 46 | token = "" # Token oder false, false ohne " " schreiben 47 | 48 | 49 | [mess_cfg] 50 | # Auswahl des verwendeten Models 51 | # Verfügbar: DDS353B, SDM72DM, SDM230, SDM530, SDM630 52 | device = "" 53 | messintervall = 10 # in Sekunden 54 | schnelles_messintervall = 1 # in Sekunden 55 | dauer_schnelles_messintervall = 30 # in Sekunden 56 | intervall_daten_senden = 30 # in Sekunden 57 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | # Smartmeter Konfigurationsfile 132 | smartmeter_cfg.toml 133 | -------------------------------------------------------------------------------- /smartmeter_telegrambot.py: -------------------------------------------------------------------------------- 1 | import telegram_api.telegram_bot_api as api 2 | import subprocess 3 | import shlex 4 | 5 | 6 | class SmartmeterBot: 7 | def __init__(self, token, logger): 8 | self.bot = api.Bot(token) 9 | self.logger = logger 10 | self.offset = 0 11 | 12 | def get_updates(self): 13 | """ 14 | Hole neue Nachrichten vom Telegramserver ab und quittiere diese 15 | :return: 16 | """ 17 | self.bot.get_updates(self.offset) 18 | self.logger.debug(self.bot.result) 19 | # Wenn das Update erfolgreich ist, das Ergebnis vom Objekt in lokale Variable übertragen und auswerten 20 | if self.bot.result["ok"]: 21 | result = self.bot.result["result"] 22 | self.logger.debug("Das Update enthält {anzahl} Nachrichten".format(anzahl=len(result))) 23 | update_id = [] 24 | for counter, nachricht in enumerate(result): 25 | self.logger.debug("Inhalt Nachricht {nr}: {inhalt}".format(nr=counter, inhalt=nachricht)) 26 | nachrichten_handler(nachricht, self.bot) 27 | update_id.append(nachricht["update_id"]) 28 | # Die größte update_id um 1 erhöhen und als Offsetwert für nächste Abfrage speichern 29 | if len(result) != 0: 30 | self.offset = max(update_id) + 1 31 | else: 32 | self.offset = 0 33 | else: 34 | self.logger.warning("Telegram Abruf fehlgeschlagen") 35 | self.bot.get_updates(self.offset) 36 | 37 | 38 | def nachrichten_handler(nachricht, bot): 39 | """Handling der vorliegenden Nachricht""" 40 | telegramid = nachricht["message"]["from"]["id"] 41 | if "message" in nachricht: 42 | # Prüfen ob es sich um ein Botkommando handelt 43 | if "bot_command" in nachricht["message"].get("entities", [{}])[0].get("type", ""): 44 | bot_command(nachricht, bot, telegramid) 45 | 46 | 47 | # --------------------------------------------------------------------------------------------------------------------- 48 | # Ab hier kommen die Botkommandos 49 | # --------------------------------------------------------------------------------------------------------------------- 50 | def bot_command(nachricht, bot, telegramid): 51 | """Hier werden alle Verfügbaren Telegramkommdos angelegt""" 52 | kommando = nachricht["message"]["text"] 53 | if kommando == "/start": 54 | pass 55 | elif kommando == "/schnelles_messintervall": 56 | schnelles_messintervall() 57 | bot.send_message(telegramid, "Intervall verkürzt") 58 | 59 | 60 | def schnelles_messintervall(): 61 | command = "pkill -f smartmeter -USR2" 62 | subprocess.run(shlex.split(command)) 63 | -------------------------------------------------------------------------------- /setup_logging.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | """ 3 | Beschreibung: 4 | Hilfskript zum Erstellen einer logging Instanz in Abhängigkeit, ob das Skript manuell gestartet worden ist oder 5 | ob das Skript per Service Unit gestartet wurde 6 | Abhängig dessen wird entweder die Logging Instanz mit einem JournalHandler erstellt (Service Unit) 7 | oder mit einem StreamHandler (manuell) 8 | 9 | Author: Hofei 10 | Datum: 03.08.2018 11 | Version: 0.1 12 | """ 13 | import logging 14 | import os 15 | import shlex 16 | import subprocess 17 | 18 | from systemd import journal 19 | 20 | 21 | def __setup_logging(loglevel, frm, startmethode, unitname): 22 | """ 23 | Erstellt die Logger Instanz für das Skript 24 | """ 25 | logger = logging.getLogger() 26 | logger.setLevel(loglevel) 27 | logger.handlers = [] 28 | if startmethode == "auto": 29 | log_handler = journal.JournalHandler(SYSLOG_IDENTIFIER=unitname) 30 | 31 | else: 32 | log_handler = logging.StreamHandler() 33 | log_handler.setLevel(loglevel) 34 | log_handler.setFormatter(frm) 35 | logger.addHandler(log_handler) 36 | return logger 37 | 38 | 39 | def __get_service_unit_pid(unitname): 40 | """Ermittelt ob das ausführende Skript mit einer Service Unit gestartet worden ist, wenn ja so ist das 41 | Ergebnis (pid_service_unit) != 0""" 42 | cmd = "systemctl show -p MainPID {}".format(unitname) 43 | cmd = shlex.split(cmd) 44 | antwort = subprocess.run(cmd, stdout=subprocess.PIPE) 45 | ausgabe = antwort.stdout 46 | # strip entfernt \n, split teilt am = in eine Liste und [1] weißt die Zahl in die Variable zu 47 | pid_service_unit = int(ausgabe.decode().strip().split("=")[1]) 48 | return pid_service_unit 49 | 50 | 51 | def __get_startmethode(unitname): 52 | """Verglicht die PID vom skript mit der pid Service Unit Prüfung 53 | wenn die Nummern gleich sind wird auf auto gestellt, wenn nicht auf manuell""" 54 | pid_service_unit = __get_service_unit_pid(unitname) 55 | pid_skript = os.getpid() 56 | if pid_service_unit == pid_skript: 57 | startmethode = "auto" 58 | else: 59 | startmethode = "manuell" 60 | return startmethode 61 | 62 | 63 | def __set_loggerformat(startmethode): 64 | """Stellt die passende Formattierung ein""" 65 | if startmethode == "auto": 66 | frm = logging.Formatter("%(levelname)s: %(message)s", "%d.%m.%Y %H:%M:%S") 67 | else: 68 | frm = logging.Formatter("%(asctime)s %(levelname)s: %(message)s", "%d.%m.%Y %H:%M:%S") 69 | return frm 70 | 71 | 72 | def create_logger(unitname, loglevel): 73 | """Dies ist die aufzurufende Funktion bei der Verwendung des Moduls von außen 74 | Liefert die fertige Logging Instanz zurück""" 75 | startmethode = __get_startmethode(unitname) 76 | frm = __set_loggerformat(startmethode) 77 | return __setup_logging(loglevel, frm, startmethode, unitname) 78 | 79 | 80 | if __name__ == "__main__": 81 | logger = create_logger("testunit", 10) 82 | logger.debug("Testnachricht") 83 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Smartmeter 2 | 3 | ## Ziel des Projektes: 4 | 5 | Stromzähler/Smartmeter via ModBus und Raspberry Pi auslesen, die Werte in einer Datenbank speichern und mit Grafana visualisieren 6 | 7 | # Vorbereitungen 8 | 9 | ## Benötigte Hardware 10 | 11 | * Verwendeter Stromzähler: 12 | SDM530 von bg-etech 13 | 14 | Ebenso möglich ist der Typ DDS353B, [SDM230](https://stromzähler.eu/stromzaehler/wechselstromzaehler/fuer-hutschiene-geeicht/216/sdm230modbus-mid-1phasen-2te-lcd-wechselstromzaehler?number=1121216-M22), [SDM630](https://stromzähler.eu/stromzaehler/drehstromzaehler/fuer-hutschiene-geeicht/120/sdm630modbus-v2-mid-zweirichtungs-multifunktionsstromzaehler-mit-rs485-und-2x-s0?number=1141213-M22) (hier Danke für die Integration an 15 | [rpi-joe](https://forum-raspberrypi.de/user/5786-rpi-joe/) aus dem deutschen Raspberry Pi Forum) 16 | * Raspberry Pi mit Zubehör 17 | * USB ModBus Adapter: z.B. [hier](https://www.ebay.de/itm/255283310832) 18 | 19 | Gibts auch billiger, aber da war mir die Wartezeit im Verhältnis zum Preis zu hoch 20 | * geschirmtes Buskabel (lt. Anleitung vom Stromzähler) 21 | * 2x 120Ohm 1/4Watt Abschlusswiderstand 22 | 23 | **Einbau des Stromzählers nur durch Elektrofachpersonal! 24 | Angaben ohne Gewähr! Besser nochmals nach Anleitung prüfen** 25 | 26 | ## Benötigte Software 27 | 28 | * Python 3.7 oder höher 29 | * Grafana zur Visualisierung 30 | 31 | ### Unterstützte Datenbanken 32 | Direkte Verbindung zu 33 | * sqlite3 34 | * mySQL 35 | * PostgreSQL (optional mit timescale) 36 | 37 | Für PostgreSQL gibt es noch eine weitere Möglichkeit der Datenübertragung: 38 | * [postgrest](https://postgrest.org/en/v6.0/) 39 | 40 | Postgrest ermöglicht die Datenübertragung über eine Web-API (muss vom Server natürlich bereitgestellt werden) 41 | 42 | ### Benötigte Python Module 43 | 44 | * Toml 45 | * Peewee 46 | * serial 47 | * minimalmodbus 48 | 49 | Installation dieser: 50 | 51 | Apt Installation erfordert ggf. root Rechte! Paketquellen zuvor updaten. (apt update) 52 | 53 | ```console 54 | apt install build-essential libssl-dev libffi-dev python3-dev libpq5 git 55 | git clone https://github.com/Hofei90/smartmeter.git /home/pi/smartmeter 56 | cd /home/pi/smartmeter 57 | pip3 install --user -r requirements.txt 58 | git submodule init && git submodule update 59 | ``` 60 | 61 | Wird als Datenbank **mysql/mariadb** verwendet, so muss noch folgendes Paket installiert werden 62 | ```console 63 | pip3 install --user PyMySQL 64 | ``` 65 | 66 | Wird als Datenbank **PostgreSQL**, so muss noch folgendes Paket installiert werden 67 | ```console 68 | pip3 install --user psycopg2 69 | ``` 70 | 71 | Für **sqlite3** ist keine weitere Installation notwendig. 72 | 73 | 74 | ### Telegram (Optional) 75 | Ist der Telegrambot nicht erwünscht, so muss in der Konfigurationsdatei `false` eingetragen werden. 76 | Aktuell ist es nur möglich, mit dem Bot das Messintervall zu verkürzen. 77 | 78 | 79 | ### MQTT (Optional) 80 | Wird eine Datenübertragung an MQTT gewünscht, so muss in der Konfigurationsdatei is_active auf `true` gesetzt werden 81 | und die Konfiguration für MQTT ausgefüllt werden. 82 | 83 | ## Programm einrichten 84 | 85 | ### Konfiguration anpassen 86 | 87 | ```console 88 | cp smartmeter_cfg_vorlage.toml smartmeter_cfg.toml 89 | ``` 90 | 91 | Anschließend die `smartmeter_cfg.toml` anpassen. 92 | Konfigurationsdatei muss im selben Ordner wie die Skripte mit dem Namen `smartmeter_cfg.toml` gespeichert werden 93 | Bei der Anpassung sind `< >` zu entfernen - `" "` müssen stehen bleiben. 94 | 95 | 96 | ## Inbetriebnahme 97 | 98 | ### Erstmaliger Test: 99 | 100 | `python3 smartmeter.py` ausführen 101 | 102 | Wenn dieser Erfolgreich verläuft, erhält man folgende Meldung 103 | 104 | ```jsunicoderegexp 105 | 18.03.2021 10:27:45 INFO: Durchlaufintervall in Config aktualisiert 106 | Programm wird beendet. Bitte neu starten 107 | ``` 108 | Bevor nun das Programm neu gestartet wird, nochmals die Konfigurationsdatei öffnen und die Einträge bei 109 | Durchlaufintervall prüfen und den eigenen Wünschen anpassen. 110 | Soll ein Parameter nie gemessen werden, so ist der Wert auf `false` zu stellen. 111 | Ansonsten angeben, bei dem wievielten Durchlauf der entsprechenden Wert jeweils ausgelesen und gespeichert werden soll. 112 | 113 | Nun das Programm erneut starten, erscheint anschließend keine Fehlermeldung, so kann eine Service Unit für den Autostart 114 | erstellt werden. 115 | 116 | ### Service Unit erstellen 117 | 118 | Ausführung erfordert root Rechte 119 | 120 | `nano /etc/systemd/system/smartmeter.service` 121 | 122 | ```code 123 | # Pfad zum speichern: /etc/systemd/system/smartmeter.service 124 | [Unit] 125 | Description=ServiceUnit zum starten des Smartmeters 126 | After=network.target 127 | 128 | [Service] 129 | Type=simple 130 | ExecStart=/usr/bin/python3 /home/pi/smartmeter/smartmeter.py 131 | User=pi 132 | 133 | 134 | [Install] 135 | WantedBy=multi-user.target 136 | ``` 137 | 138 | `systemctl start smartmeter.service` 139 | 140 | Kontrolle ob Skript nun wieder aktiv ist, wenn ja automatische Ausführung anlegen: 141 | 142 | `systemctl enable smartmeter.service` 143 | 144 | ## Grafana 145 | 146 | Die Visualisierung findet in Grafana statt, auf nähere Ausführungen wird hier jedoch verzichtet, natürlich können die 147 | Messwerte auch mit anderen Tools visualsiert werden. 148 | -------------------------------------------------------------------------------- /smartmeter.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import os 3 | import signal 4 | import time 5 | import traceback 6 | from copy import deepcopy 7 | from sys import exit 8 | 9 | import toml 10 | 11 | import electric_meter 12 | import setup_logging 13 | import db_model as db 14 | import db_postgrest_model as db_postgrest 15 | 16 | 17 | CONFIGDATEI = "smartmeter_cfg.toml" 18 | FEHLERDATEI = "fehler_smartmeter.log" 19 | SKRIPTPFAD = os.path.abspath(os.path.dirname(__file__)) 20 | 21 | 22 | def load_config(): 23 | """Laedt die Konfiguration aus dem smartmeter_cfg.toml File""" 24 | configfile = os.path.join(SKRIPTPFAD, CONFIGDATEI) 25 | with open(configfile) as conffile: 26 | config = toml.loads(conffile.read()) 27 | return config 28 | 29 | 30 | LOGGER = setup_logging.create_logger("smartmeter", 20) 31 | CONFIG = load_config() 32 | 33 | if CONFIG["telegram_bot"]["token"]: 34 | from smartmeter_telegrambot import SmartmeterBot 35 | 36 | # Model Initialisierung nur wenn is_active ist True 37 | if CONFIG["mqtt"]["is_active"]: 38 | import mqtt 39 | MQTT_ = mqtt.MQTT(broker=CONFIG["mqtt"]["broker"], 40 | port=CONFIG["mqtt"]["port"], 41 | topic=CONFIG["mqtt"]["topic"], 42 | username=CONFIG["mqtt"]["username"], 43 | password=CONFIG["mqtt"]["password"]) 44 | 45 | 46 | class MessHandler: 47 | """ 48 | MessHandler ist zustaendig für das organiesieren, dass Messwerte ausgelesen, gespeichert und in die Datenbank 49 | geschrieben werden. 50 | """ 51 | def __init__(self, messregister): 52 | self.schnelles_messen = False 53 | self.startzeit_schnelles_messen = datetime.datetime(1970, 1, 1, tzinfo=datetime.timezone.utc) 54 | self.messregister = messregister 55 | self.messregister_save = deepcopy(messregister) 56 | self.messwerte_liste = [] 57 | self.intervall_daten_senden = CONFIG["mess_cfg"]["intervall_daten_senden"] 58 | self.pausenzeit = CONFIG["mess_cfg"]["messintervall"] 59 | 60 | def set_schnelles_messintervall(self, *_): 61 | """Kommt von außerhalb das Signal USR2 wird das Mess und Sendeintervall verkuerzt""" 62 | self.schnelles_messen = True 63 | self.startzeit_schnelles_messen = datetime.datetime.now(datetime.timezone.utc) 64 | self.intervall_daten_senden = CONFIG["mess_cfg"]["schnelles_messintervall"] 65 | self.pausenzeit = CONFIG["mess_cfg"]["schnelles_messintervall"] 66 | 67 | def off_schnelles_messintervall(self): 68 | """Mess und Sendeintervall wird wieder auf Standardwerte zurueckgesetzt""" 69 | self.schnelles_messen = False 70 | self.startzeit_schnelles_messen = datetime.datetime(1970, 1, 1, tzinfo=datetime.timezone.utc) 71 | self.intervall_daten_senden = CONFIG["mess_cfg"]["intervall_daten_senden"] 72 | self.pausenzeit = CONFIG["mess_cfg"]["messintervall"] 73 | 74 | def add_messwerte(self, messwerte): 75 | """Speichern der Messwerte zwischen bis zu Ihrer Uebertragung""" 76 | self.messwerte_liste.append(messwerte) 77 | 78 | def schreibe_messwerte(self, datenbankschnittstelle): 79 | """Gespeicherte Messwerte in die Datenbank schreiben""" 80 | LOGGER.debug("Sende Daten") 81 | datenbankschnittstelle.insert_many(self.messwerte_liste) 82 | 83 | if CONFIG["mqtt"]["is_active"]: 84 | MQTT_.send(self.messwerte_liste, LOGGER) 85 | 86 | self.messwerte_liste = [] 87 | 88 | def erstelle_auszulesende_messregister(self): 89 | """Prueft welche Messwerte nach Ihren Intervalleinstellungen im aktuellen Durchlauf ausgelesen werden muessen""" 90 | if self.schnelles_messen: 91 | return [key for key in self.messregister] 92 | else: 93 | return [key for key in self.messregister if self.messregister[key]["verbleibender_durchlauf"] <= 1] 94 | 95 | def reduziere_durchlauf_anzahl(self): 96 | for key in self.messregister: 97 | self.messregister[key]["verbleibender_durchlauf"] -= 1 98 | 99 | def durchlauf_zuruecksetzen(self, messauftrag): 100 | for key in messauftrag: 101 | self.messregister[key]["verbleibender_durchlauf"] = deepcopy(self.messregister[key]["intervall"]) 102 | 103 | 104 | class Datenbankschnittstelle: 105 | def __init__(self, db_adapter, device): 106 | self.db_tables = [db.get_smartmeter_table(device)] 107 | self.db_table = self.db_tables[0] 108 | 109 | self.db_adapter = db_adapter 110 | if db_adapter == "postgrest": 111 | self.headers = {f"Authorization": "{user} {token}".format(user=CONFIG["db"]["postgrest"]["user"], 112 | token=CONFIG["db"]["postgrest"]["token"])} 113 | url = CONFIG["db"]["postgrest"]["url"] 114 | if not url.endswith("/"): 115 | url = f"{url}/" 116 | self.url = "{url}{table}".format(url=url, 117 | table=CONFIG["db"]["postgrest"]["table"]) 118 | self.none_messdaten = self.__none_messdaten_dictionary_erstellen() 119 | else: 120 | self.headers = None 121 | self.url = None 122 | db_adapter = CONFIG["db"]["db"] 123 | db_ = db.init_db(CONFIG["db"][db_adapter]["database"], db_adapter, CONFIG["db"].get(db_adapter)) 124 | db.DB_PROXY.initialize(db_) 125 | db.create_tables(self.db_tables) 126 | 127 | def insert_many(self, daten): 128 | if self.db_adapter == "postgrest": 129 | db_postgrest.sende_daten(self.url, self.headers, daten, self.none_messdaten, LOGGER) 130 | else: 131 | db.insert_many(daten, self.db_table) 132 | 133 | @staticmethod 134 | def __none_messdaten_dictionary_erstellen(): 135 | none_daten = {"ts": None} 136 | for key in CONFIG["durchlaufintervall"]: 137 | none_daten[key.lower()] = None 138 | return none_daten 139 | 140 | 141 | def schreibe_config(config, configfile): 142 | with open(configfile, "a", encoding="UTF-8") as file: 143 | file.write(f"# Nach dem wievielten Durchlauf der jeweilige Wert ausgelesen werden soll \n" 144 | f"# Ausschalten mit false\n" 145 | f"# Eintraege werden automatisch bei dem ersten Start erstellt, Config anschließend nochmal prüfen\n" 146 | f"{toml.dumps(config)}") 147 | LOGGER.info("Durchlaufintervall in Config aktualisiert \n Programm wird beendet. Bitte neu starten") 148 | global nofailure 149 | nofailure = True 150 | exit(0) 151 | 152 | 153 | def erzeuge_durchlaufintervall(smartmeter): 154 | register = smartmeter.get_input_keys() 155 | durchlaufintervall = {} 156 | for key in register: 157 | durchlaufintervall[key] = 1 158 | config = {"durchlaufintervall": durchlaufintervall} 159 | schreibe_config(config, CONFIGDATEI) 160 | 161 | 162 | def erzeuge_messregister(smartmeter): 163 | """Erzeugt das messregister nach dem Start des Skriptes""" 164 | if "durchlaufintervall" in CONFIG: 165 | messregister = {} 166 | for key, value in CONFIG["durchlaufintervall"].items(): 167 | if value: 168 | messregister[key] = {} 169 | messregister[key]["intervall"] = value 170 | messregister[key]["verbleibender_durchlauf"] = 0 171 | return messregister 172 | else: 173 | erzeuge_durchlaufintervall(smartmeter) 174 | 175 | 176 | def fehlermeldung_schreiben(fehlermeldung): 177 | """ 178 | Schreibt nicht abgefangene Fehlermeldungen in eine sperate Datei, um so leichter Fehlermeldungen ausfindig machen zu 179 | koennen welche noch Abgefangen werden muessen. 180 | """ 181 | with open(os.path.join(SKRIPTPFAD, FEHLERDATEI), "a") as file: 182 | file.write(fehlermeldung) 183 | 184 | 185 | def main(): 186 | device = electric_meter.get_device_list().get(CONFIG["mess_cfg"]["device"]) 187 | smartmeter = device(serial_if=CONFIG["modbus"]["serial_if"], 188 | serial_if_baud=CONFIG["modbus"]["serial_if_baud"], 189 | serial_if_byte=CONFIG["modbus"]["serial_if_byte"], 190 | serial_if_par=CONFIG["modbus"]["serial_if_par"], 191 | serial_if_stop=CONFIG["modbus"]["serial_if_stop"], 192 | slave_addr=CONFIG["modbus"]["slave_addr"], 193 | timeout=CONFIG["modbus"]["timeout"], 194 | logger=LOGGER) 195 | 196 | messregister = erzeuge_messregister(smartmeter) 197 | messhandler = MessHandler(messregister) 198 | 199 | datenbankschnittstelle = Datenbankschnittstelle(CONFIG["db"]["db"], CONFIG["mess_cfg"]["device"]) 200 | 201 | if CONFIG["telegram_bot"]["token"]: 202 | telegram_bot = SmartmeterBot(CONFIG["telegram_bot"]["token"], LOGGER) 203 | else: 204 | telegram_bot = None 205 | 206 | # SIGUSR2 setzt das schnelle Messintervall 207 | signal.signal(signal.SIGUSR2, messhandler.set_schnelles_messintervall) 208 | 209 | zeitpunkt_daten_gesendet = datetime.datetime(1970, 1, 1, tzinfo=datetime.timezone.utc) 210 | start_messzeitpunkt = datetime.datetime(1970, 1, 1, tzinfo=datetime.timezone.utc) 211 | 212 | LOGGER.info("Initialisierung abgeschlossen - Start Messungen") 213 | 214 | while True: 215 | now = datetime.datetime.now(datetime.timezone.utc) 216 | now = now.replace(microsecond=0) 217 | 218 | # Prüfen, ob schnelles Messen aktiv ist und ob dies wieder auf Standard zurückgesetzt werden muss 219 | if messhandler.schnelles_messen: 220 | if (now - messhandler.startzeit_schnelles_messen).total_seconds() > \ 221 | CONFIG["mess_cfg"]["dauer_schnelles_messintervall"]: 222 | messhandler.off_schnelles_messintervall() 223 | 224 | if (now - start_messzeitpunkt).total_seconds() > messhandler.pausenzeit: 225 | 226 | # Prüfe, welche Messwerte auszulesen sind 227 | messauftrag = messhandler.erstelle_auszulesende_messregister() 228 | 229 | # Messauftrag abarbeiten und Zeitpunk ergänzen 230 | if messauftrag: 231 | start_messzeitpunkt = datetime.datetime.now(datetime.timezone.utc) 232 | messwerte = smartmeter.read_input_values(messauftrag) 233 | LOGGER.debug("Messdauer: {}".format(datetime.datetime.now(datetime.timezone.utc) - start_messzeitpunkt)) 234 | messwerte["ts"] = now 235 | messhandler.add_messwerte(messwerte) 236 | 237 | if not messhandler.schnelles_messen: 238 | messhandler.reduziere_durchlauf_anzahl() 239 | messhandler.durchlauf_zuruecksetzen(messauftrag) 240 | if telegram_bot is not None: 241 | telegram_bot.get_updates() 242 | 243 | # Schreibe die Messdaten in die Datenbank nach eingestellten Intervall 244 | if (now - zeitpunkt_daten_gesendet).total_seconds() > messhandler.intervall_daten_senden: 245 | start_schreiben = datetime.datetime.now(datetime.timezone.utc) 246 | messhandler.schreibe_messwerte(datenbankschnittstelle) 247 | LOGGER.debug("DB Dauer schreiben: {}".format( 248 | datetime.datetime.now(datetime.timezone.utc) - start_schreiben)) 249 | zeitpunkt_daten_gesendet = now 250 | LOGGER.debug("Durchlaufdauer: {}".format(datetime.datetime.now(datetime.timezone.utc) - now)) 251 | time.sleep(0.2) 252 | 253 | 254 | if __name__ == "__main__": 255 | nofailure = False 256 | try: 257 | main() 258 | finally: 259 | if not nofailure: 260 | fehlermeldung_schreiben(traceback.format_exc()) 261 | LOGGER.exception("Schwerwiegender Fehler aufgetreten") 262 | -------------------------------------------------------------------------------- /db_model.py: -------------------------------------------------------------------------------- 1 | import peewee 2 | 3 | DB_PROXY = peewee.Proxy() 4 | 5 | 6 | class BaseModel(peewee.Model): 7 | class Meta: 8 | database = DB_PROXY 9 | 10 | 11 | class DDS353B(BaseModel): 12 | ts = peewee.DateTimeField(primary_key=True) 13 | power = peewee.FloatField(null=True) 14 | 15 | 16 | class SDM72DM(BaseModel): 17 | ts = peewee.DateTimeField(primary_key=True) 18 | aktuelle_gesamtwirkleistung = peewee.FloatField(null=True) 19 | import_wh_seit_reset = peewee.FloatField(null=True) 20 | export_wh_seit_reset = peewee.FloatField(null=True) 21 | total_kwh = peewee.FloatField(null=True) 22 | settable_total_kwh = peewee.FloatField(null=True) 23 | settable_import_kwh = peewee.FloatField(null=True) 24 | setabble_export_kwh = peewee.FloatField(null=True) 25 | import_power = peewee.FloatField(null=True) 26 | export_power = peewee.FloatField(null=True) 27 | 28 | 29 | class SDM230(BaseModel): 30 | ts = peewee.DateTimeField(primary_key=True) 31 | spannung_l1 = peewee.FloatField(null=True) 32 | strom_l1 = peewee.FloatField(null=True) 33 | wirkleistung_l1 = peewee.FloatField(null=True) 34 | scheinleistung_l1 = peewee.FloatField(null=True) 35 | blindleistung_l1 = peewee.FloatField(null=True) 36 | leistungsfaktor_l1 = peewee.FloatField(null=True) 37 | phasenwinkel_l1 = peewee.FloatField(null=True) 38 | frequenz = peewee.FloatField(null=True) 39 | import_wh_seit_reset = peewee.FloatField(null=True) 40 | export_wh_seit_reset = peewee.FloatField(null=True) 41 | import_varh_seit_reset = peewee.FloatField(null=True) 42 | export_varh_seit_reset = peewee.FloatField(null=True) 43 | gesamtwirkleistung = peewee.FloatField(null=True) 44 | max_gesamtwirkleistung = peewee.FloatField(null=True) 45 | currentsystempositivepowerdemand = peewee.FloatField(null=True) 46 | maximumsystempositivepowerdemand = peewee.FloatField(null=True) 47 | currentsystemreversepowerdemand = peewee.FloatField(null=True) 48 | strom_l1_demand = peewee.FloatField(null=True) 49 | max_strom_l1_demand = peewee.FloatField(null=True) 50 | total_kwh = peewee.FloatField(null=True) 51 | total_kvarh = peewee.FloatField(null=True) 52 | 53 | 54 | class SDM530(BaseModel): 55 | ts = peewee.DateTimeField(primary_key=True) 56 | ah_seit_reset = peewee.FloatField(null=True) 57 | aktuelle_gesamtblindleistung = peewee.FloatField(null=True) 58 | aktuelle_gesamtscheinleistung = peewee.FloatField(null=True) 59 | aktuelle_gesamtwirkleistung = peewee.FloatField(null=True) 60 | aktueller_gesamtleistungsfaktor = peewee.FloatField(null=True) 61 | aktueller_gesamtphasenwinkel = peewee.FloatField(null=True) 62 | aktueller_gesamtstrom = peewee.FloatField(null=True) 63 | blindleistung_l1 = peewee.FloatField(null=True) 64 | blindleistung_l2 = peewee.FloatField(null=True) 65 | blindleistung_l3 = peewee.FloatField(null=True) 66 | durchschnittliche_spannung_zu_n = peewee.FloatField(null=True) 67 | durchschnittlicher_strom_zu_n = peewee.FloatField(null=True) 68 | durchschnittsspannung_l_l = peewee.FloatField(null=True) 69 | export_varh_seit_reset = peewee.FloatField(null=True) 70 | export_wh_seit_reset = peewee.FloatField(null=True) 71 | frequenz = peewee.FloatField(null=True) 72 | gesamtscheinleistung = peewee.FloatField(null=True) 73 | gesamtstrom_neutralleiter = peewee.FloatField(null=True) 74 | gesamtsystemleistungsfaktor = peewee.FloatField(null=True) 75 | gesamtwirkleistung = peewee.FloatField(null=True) 76 | import_varh_seit_reset = peewee.FloatField(null=True) 77 | import_wh_seit_reset = peewee.FloatField(null=True) 78 | leistungsfaktor_l1 = peewee.FloatField(null=True) 79 | leistungsfaktor_l2 = peewee.FloatField(null=True) 80 | leistungsfaktor_l3 = peewee.FloatField(null=True) 81 | max_gesamtscheinleistung = peewee.FloatField(null=True) 82 | max_gesamtwirkleistung = peewee.FloatField(null=True) 83 | max_strom_l1_demand = peewee.FloatField(null=True) 84 | max_strom_l2_demand = peewee.FloatField(null=True) 85 | max_strom_l3_demand = peewee.FloatField(null=True) 86 | max_strom_neutralleiter = peewee.FloatField(null=True) 87 | phasenwinkel_l1 = peewee.FloatField(null=True) 88 | phasenwinkel_l2 = peewee.FloatField(null=True) 89 | phasenwinkel_l3 = peewee.FloatField(null=True) 90 | scheinleistung_l1 = peewee.FloatField(null=True) 91 | scheinleistung_l2 = peewee.FloatField(null=True) 92 | scheinleistung_l3 = peewee.FloatField(null=True) 93 | spannung_l1 = peewee.FloatField(null=True) 94 | spannung_l1_l2 = peewee.FloatField(null=True) 95 | spannung_l2 = peewee.FloatField(null=True) 96 | spannung_l2_l3 = peewee.FloatField(null=True) 97 | spannung_l3 = peewee.FloatField(null=True) 98 | spannung_l3_l1 = peewee.FloatField(null=True) 99 | strom_l1 = peewee.FloatField(null=True) 100 | strom_l1_demand = peewee.FloatField(null=True) 101 | strom_l2 = peewee.FloatField(null=True) 102 | strom_l2_demand = peewee.FloatField(null=True) 103 | strom_l3 = peewee.FloatField(null=True) 104 | strom_l3_demand = peewee.FloatField(null=True) 105 | strom_neutralleiter = peewee.FloatField(null=True) 106 | thd_durchschnittliche_spannung_zu_l_l = peewee.FloatField(null=True) 107 | thd_durchschnittliche_spannung_zu_n = peewee.FloatField(null=True) 108 | thd_durchschnittlicher_strom_zu_n = peewee.FloatField(null=True) 109 | thd_spannung_l1 = peewee.FloatField(null=True) 110 | thd_spannung_l1_l2 = peewee.FloatField(null=True) 111 | thd_spannung_l2 = peewee.FloatField(null=True) 112 | thd_spannung_l2_l3 = peewee.FloatField(null=True) 113 | thd_spannung_l3 = peewee.FloatField(null=True) 114 | thd_spannung_l3_l1 = peewee.FloatField(null=True) 115 | thd_strom_l1 = peewee.FloatField(null=True) 116 | thd_strom_l2 = peewee.FloatField(null=True) 117 | thd_strom_l3 = peewee.FloatField(null=True) 118 | total_kvarh = peewee.FloatField(null=True) 119 | total_kwh = peewee.FloatField(null=True) 120 | vah_seit_reset = peewee.FloatField(null=True) 121 | wirkleistung_l1 = peewee.FloatField(null=True) 122 | wirkleistung_l2 = peewee.FloatField(null=True) 123 | wirkleistung_l3 = peewee.FloatField(null=True) 124 | 125 | 126 | class SDM630(BaseModel): 127 | ts = peewee.DateTimeField(primary_key=True) 128 | spannung_l1 = peewee.FloatField(null=True) 129 | spannung_l2 = peewee.FloatField(null=True) 130 | spannung_l3 = peewee.FloatField(null=True) 131 | strom_l1 = peewee.FloatField(null=True) 132 | strom_l2 = peewee.FloatField(null=True) 133 | strom_l3 = peewee.FloatField(null=True) 134 | wirkleistung_l1 = peewee.FloatField(null=True) 135 | wirkleistung_l2 = peewee.FloatField(null=True) 136 | wirkleistung_l3 = peewee.FloatField(null=True) 137 | scheinleistung_l1 = peewee.FloatField(null=True) 138 | scheinleistung_l2 = peewee.FloatField(null=True) 139 | scheinleistung_l3 = peewee.FloatField(null=True) 140 | blindleistung_l1 = peewee.FloatField(null=True) 141 | blindleistung_l2 = peewee.FloatField(null=True) 142 | blindleistung_l3 = peewee.FloatField(null=True) 143 | leistungsfaktor_l1 = peewee.FloatField(null=True) 144 | leistungsfaktor_l2 = peewee.FloatField(null=True) 145 | leistungsfaktor_l3 = peewee.FloatField(null=True) 146 | phasenwinkel_l1 = peewee.FloatField(null=True) 147 | phasenwinkel_l2 = peewee.FloatField(null=True) 148 | phasenwinkel_l3 = peewee.FloatField(null=True) 149 | durchschnittliche_spannung_zu_n = peewee.FloatField(null=True) 150 | durchschnittlicher_strom_zu_n = peewee.FloatField(null=True) 151 | aktueller_gesamtstrom = peewee.FloatField(null=True) 152 | aktuelle_gesamtwirkleistung = peewee.FloatField(null=True) 153 | aktuelle_gesamtscheinleistung = peewee.FloatField(null=True) 154 | aktuelle_gesamtblindleistung = peewee.FloatField(null=True) 155 | aktueller_gesamtleistungsfaktor = peewee.FloatField(null=True) 156 | aktueller_gesamtphasenwinkel = peewee.FloatField(null=True) 157 | frequenz = peewee.FloatField(null=True) 158 | import_wh_seit_reset = peewee.FloatField(null=True) 159 | export_wh_seit_reset = peewee.FloatField(null=True) 160 | import_varh_seit_reset = peewee.FloatField(null=True) 161 | export_varh_seit_reset = peewee.FloatField(null=True) 162 | vah_seit_reset = peewee.FloatField(null=True) 163 | ah_seit_reset = peewee.FloatField(null=True) 164 | gesamtwirkleistung = peewee.FloatField(null=True) 165 | max_gesamtwirkleistung = peewee.FloatField(null=True) 166 | gesamtscheinleistung = peewee.FloatField(null=True) 167 | max_gesamtscheinleistung = peewee.FloatField(null=True) 168 | gesamtstrom_neutralleiter = peewee.FloatField(null=True) 169 | max_strom_neutralleiter = peewee.FloatField(null=True) 170 | spannung_l1_l2 = peewee.FloatField(null=True) 171 | spannung_l2_l3 = peewee.FloatField(null=True) 172 | spannung_l3_l1 = peewee.FloatField(null=True) 173 | durchschnittsspannung_l_l = peewee.FloatField(null=True) 174 | strom_neutralleiter = peewee.FloatField(null=True) 175 | thd_spannung_l1 = peewee.FloatField(null=True) 176 | thd_spannung_l2 = peewee.FloatField(null=True) 177 | thd_spannung_l3 = peewee.FloatField(null=True) 178 | thd_strom_l1 = peewee.FloatField(null=True) 179 | thd_strom_l2 = peewee.FloatField(null=True) 180 | thd_strom_l3 = peewee.FloatField(null=True) 181 | thd_durchschnittliche_spannung_zu_n = peewee.FloatField(null=True) 182 | thd_durchschnittlicher_strom_zu_n = peewee.FloatField(null=True) 183 | strom_l1_demand = peewee.FloatField(null=True) 184 | strom_l2_demand = peewee.FloatField(null=True) 185 | strom_l3_demand = peewee.FloatField(null=True) 186 | max_strom_l1_demand = peewee.FloatField(null=True) 187 | max_strom_l2_demand = peewee.FloatField(null=True) 188 | max_strom_l3_demand = peewee.FloatField(null=True) 189 | thd_spannung_l1_l2 = peewee.FloatField(null=True) 190 | thd_spannung_l2_l3 = peewee.FloatField(null=True) 191 | thd_spannung_l3_l1 = peewee.FloatField(null=True) 192 | thd_durchschnittliche_spannung_zu_l_l = peewee.FloatField(null=True) 193 | total_kwh = peewee.FloatField(null=True) 194 | total_kvarh = peewee.FloatField(null=True) 195 | import_l1_kwh = peewee.FloatField(null=True) 196 | import_l2_kwh = peewee.FloatField(null=True) 197 | import_l3_kwh = peewee.FloatField(null=True) 198 | export_l1_kwh = peewee.FloatField(null=True) 199 | export_l2_kwh = peewee.FloatField(null=True) 200 | export_l3_kwh = peewee.FloatField(null=True) 201 | gesamtstrom_l1_kwh = peewee.FloatField(null=True) 202 | gesamtstrom_l2_kwh = peewee.FloatField(null=True) 203 | gesamtstrom_l3_kwh = peewee.FloatField(null=True) 204 | import_l1_kvarh = peewee.FloatField(null=True) 205 | import_l2_kvarh = peewee.FloatField(null=True) 206 | import_l3_kvarh = peewee.FloatField(null=True) 207 | export_l1_kvarh = peewee.FloatField(null=True) 208 | export_l2_kvarh = peewee.FloatField(null=True) 209 | export_l3_kvarh = peewee.FloatField(null=True) 210 | total_l1_kvarh = peewee.FloatField(null=True) 211 | total_l2_kvarh = peewee.FloatField(null=True) 212 | total_l3_kvarh = peewee.FloatField(null=True) 213 | 214 | 215 | def get_smartmeter_table(device): 216 | if device == "DDS353B": 217 | return DDS353B 218 | elif device == "SDM72DM": 219 | return SDM72DM 220 | elif device == "SDM230": 221 | return SDM230 222 | elif device == "SDM530": 223 | return SDM530 224 | elif device == "SDM630": 225 | return SDM630 226 | else: 227 | raise ValueError("Devicename falsch oder nicht unterstützt?") 228 | 229 | 230 | def insert_many(daten, db_table): 231 | daten_konvertiert = [] 232 | for datensatz in daten: 233 | datensatz_konvertiert = {} 234 | for key, value in datensatz.items(): 235 | datensatz_konvertiert[key.lower()] = value 236 | daten_konvertiert.append(datensatz_konvertiert) 237 | db_table.insert_many(daten_konvertiert).execute() 238 | 239 | 240 | def create_tables(tables): 241 | DB_PROXY.create_tables(tables) 242 | 243 | 244 | def init_db(name, type_="sqlite", config=None): 245 | config = config or {} 246 | drivers = { 247 | "sqlite": peewee.SqliteDatabase, 248 | "mysql": peewee.MySQLDatabase, 249 | "postgresql": peewee.PostgresqlDatabase, 250 | } 251 | 252 | try: 253 | cls = drivers[type_] 254 | except KeyError: 255 | raise ValueError("Unknown database type: {}".format(type_)) from None 256 | del config["database"] 257 | db = cls(name, **config) 258 | return db 259 | -------------------------------------------------------------------------------- /electric_meter.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/python3 2 | 3 | from time import sleep 4 | import serial 5 | import minimalmodbus 6 | 7 | 8 | class ModBusRTU: 9 | """ 10 | Main class for ModBus communication 11 | """ 12 | 13 | # instrument = None 14 | # log = None 15 | data = {} 16 | 17 | def __init__(self, logger, serial_if, serial_if_baud, serial_if_byte, serial_if_par, serial_if_stop, slave_addr, 18 | timeout): 19 | """ 20 | Init method of ModBusRTU. 21 | Here will be the serial modbus adapter connected and initialised. 22 | """ 23 | 24 | self.log = logger 25 | try: 26 | s = "Init with: serial_if={}, serial_if_baud={}, serial_if_byte={}, serial_if_par={}, " \ 27 | "serial_if_stop={}, slave_addr={}, timeout={}".format(serial_if, serial_if_baud, serial_if_byte, 28 | serial_if_par, serial_if_stop, slave_addr, 29 | timeout) 30 | 31 | self.log.debug(s) 32 | 33 | self.instrument = minimalmodbus.Instrument(serial_if, slave_addr) # port name, slave address (in decimal) 34 | self.instrument.serial.baudrate = serial_if_baud 35 | self.instrument.serial.bytesize = serial_if_byte 36 | self.instrument.serial.parity = serial_if_par 37 | self.instrument.serial.stopbits = serial_if_stop 38 | self.instrument.serial.timeout = timeout 39 | except serial.serialutil.SerialException as e: 40 | self.log.error("Initialisation returns an error: {}".format(e)) 41 | 42 | def read_data_point_from_meter(self, func_code=None, reg_addr=None, number_of_reg=None): 43 | """ 44 | Read a data point, defined per function code, address data and count of readable bytes. 45 | 46 | @param func_code: Function code for ModeBus 47 | @param reg_addr: Datapoint register address 48 | @param number_of_reg: Amount of byte to read 49 | 50 | @return: Returns a list of integer, the length of the list is depend of the parameter 'number_of_reg' 51 | """ 52 | assert (func_code is not None), "Error: No function code given" 53 | assert (reg_addr is not None), "Error: Register address parameter missing." 54 | assert (number_of_reg is not None), "Error: Number of registers parameter missing." 55 | 56 | if self.instrument is not None: 57 | try: 58 | scale_value_tupel = self.instrument.read_register(functioncode=func_code, registeraddress=reg_addr, 59 | number_of_decimals=number_of_reg) 60 | self.log.debug("RegAddr='{}', NoOfReg='{}', Result tuple (digit array)='{}'".format(reg_addr, 61 | number_of_reg, 62 | scale_value_tupel)) 63 | return scale_value_tupel 64 | except IOError as e: 65 | self.log.error("read_registers() returns an io error: {}".format(e.message)) 66 | else: 67 | self.log.error("No device available, measurement not possible, return None") 68 | 69 | 70 | class DDS353B(ModBusRTU): 71 | """ 72 | Driver class for energy meter 'DDS353B' 73 | This energy meter can be deliver only one value: the front displayed energy value. 74 | """ 75 | # only once of parameter can be read 76 | registerAdr = {"power": {"port": 0, "digits": 3}} # Display value (in kWh) 77 | 78 | def get_engine_values(self): 79 | # (1) power value from front display 80 | if self.instrument is not None: 81 | for key in self.registerAdr: 82 | # Register address "0", get 3 digits 83 | val_tuple = self.read_data_point_from_meter(func_code=3, reg_addr=self.registerAdr[key]["port"], 84 | number_of_reg=self.registerAdr[key]["digits"]) 85 | if val_tuple is not None: 86 | # 2 nachkommastellen 87 | power_value = (65536 * val_tuple[0] + 256 * val_tuple[1] + val_tuple[2]) / 100.0 88 | self.log.debug("Display value (val) = '{}'".format(power_value)) 89 | self.data[key] = power_value # store as float per default 90 | else: 91 | self.log.warn("Tuple value '{}' not available".format(key)) 92 | else: 93 | err_msg = "No instrument available!" 94 | self.log.error(err_msg) 95 | return None 96 | return self.data 97 | 98 | 99 | class SDM72DM(ModBusRTU): 100 | """ 101 | Driver class for energy meter 'SDM72D-M-ModBus' (B+G E-Tech EASTRON) 102 | Data Format: 4 bytes (2 registers) per parameter. Floating point format ( to IEEE 754) 103 | Most significant register first (Default). 104 | The default may be changed if required -See Holding Register "Register Order" parameter. 105 | """ 106 | 107 | def __init__(self, logger, serial_if, serial_if_baud, serial_if_byte, 108 | serial_if_par, serial_if_stop, slave_addr, timeout): 109 | super().__init__(logger, serial_if, serial_if_baud, serial_if_byte, 110 | serial_if_par, serial_if_stop, slave_addr, timeout) 111 | # Konfiguration der Input Register nach Datenblatt 112 | self.input_register = { 113 | "aktuelle_Gesamtwirkleistung": { 114 | "port": 52, "digits": 2, "Unit": "W", "use": True}, 115 | "Import_Wh_seit_reset": { 116 | "port": 72, "digits": 2, "Unit": "kWh", "use": True}, 117 | "Export_Wh_seit_reset": { 118 | "port": 74, "digits": 2, "Unit": "kWh", "use": True}, 119 | "Total_kwh": { 120 | "port": 342, "digits": 2, "Unit": "kWh", "use": True}, 121 | "Settable_total_kWh": { 122 | "port": 384, "digits": 2, "Unit": "kWh", "use": True}, 123 | "Settable_import_kWh": { 124 | "port": 388, "digits": 2, "Unit": "kWh", "use": True}, 125 | "Setabble_export_kWh": { 126 | "port": 390, "digits": 2, "Unit": "kWh", "use": True}, 127 | "Import_power": { 128 | "port": 1280, "digits": 2, "Unit": "W", "use": True}, 129 | "Export_power": { 130 | "port": 1282, "digits": 2, "Unit": "W", "use": True}, 131 | } 132 | 133 | def read_input_values(self, input_register_keys=None): 134 | """ 135 | Read all in self.input_register defined data points and stored the result as float value 136 | into self.data dictionary 137 | :return: self.data dictionary 138 | """ 139 | self.data = {} 140 | if input_register_keys is None: 141 | input_register_keys = self.get_input_keys() 142 | if self.instrument is not None: 143 | for key in input_register_keys: 144 | self.log.debug("try: key='{}', reg='{}', digits='{}'".format(key, self.input_register[key]["port"], 145 | self.input_register[key]["digits"])) 146 | if self.input_register[key]["use"] is True: 147 | 148 | fehler = 0 149 | while True: # Anzahl der Versuche 150 | try: 151 | messwert = self.instrument.read_float(functioncode=4, # fix (!) for this model 152 | registeraddress=self.input_register[key]["port"], 153 | number_of_registers=self.input_register[key][ 154 | "digits"]) 155 | except OSError: 156 | fehler += 1 157 | self.log.error("Kommunikationserror Nr. {}".format(fehler)) 158 | sleep(5) 159 | if fehler > 5: # Anzahl der Versuche 160 | raise OSError 161 | else: 162 | break 163 | 164 | if messwert is None: 165 | self.log.warn("Value '{}' not available".format(key)) 166 | else: 167 | self.data[key] = round(messwert, 4) 168 | self.log.debug("Value '{}' = '{}'".format(key, self.data[key])) 169 | else: 170 | self.log.debug("Value '{}' not used!".format(key)) 171 | pass 172 | else: 173 | err_msg = "No instrument available!" 174 | self.log.error(err_msg) 175 | return None 176 | return self.data 177 | 178 | def get_input_keys(self): 179 | """ 180 | Hilfsmethode zur Erstellung der Intervallklassen 181 | :return: 182 | """ 183 | input_register_keys = [key for key in self.input_register] 184 | return input_register_keys 185 | 186 | 187 | class SDM230(ModBusRTU): 188 | """ 189 | Driver class for energy meter 'SDM230-ModBus' (B+G E-Tech EASTRON) 190 | 191 | Data Format: 4 bytes (2 registers) per parameter. Floating point format ( to IEEE 754) 192 | Most significant register first (Default). 193 | The default may be changed if required -See Holding Register "Register Order" parameter. 194 | """ 195 | def __init__(self, logger, serial_if, serial_if_baud, serial_if_byte, 196 | serial_if_par, serial_if_stop, slave_addr, timeout): 197 | super().__init__(logger, serial_if, serial_if_baud, serial_if_byte, 198 | serial_if_par, serial_if_stop, slave_addr, timeout) 199 | # Konfiguration der Input Register nach Datenblatt 200 | self.input_register = {"Spannung_L1": { 201 | "port": 0, "digits": 2, "Unit": "V", "use": True}, 202 | "Strom_L1": 203 | {"port": 6, "digits": 2, "Unit": "A", "use": True}, 204 | "Wirkleistung_L1": 205 | {"port": 12, "digits": 2, "Unit": "W", "use": True}, 206 | "Scheinleistung_L1": 207 | {"port": 18, "digits": 2, "Unit": "VA", "use": True}, 208 | "Blindleistung_L1": 209 | {"port": 24, "digits": 2, "Unit": "VAr", "use": True}, 210 | "Leistungsfaktor_L1": 211 | {"port": 30, "digits": 2, "Unit": "", "use": True}, 212 | "Phasenwinkel_L1": 213 | {"port": 36, "digits": 2, "Unit": "Grad", "use": True}, 214 | "Frequenz": 215 | {"port": 70, "digits": 2, "Unit": "Hz", "use": True}, 216 | "Import_Wh_seit_reset": 217 | {"port": 72, "digits": 2, "Unit": "kWh", "use": True}, 218 | "Export_Wh_seit_reset": 219 | {"port": 74, "digits": 2, "Unit": "kWh", "use": True}, 220 | "Import_VArh_seit_reset": 221 | {"port": 76, "digits": 2, "Unit": "kVArh", "use": False}, 222 | "Export_VArh_seit_reset": 223 | {"port": 78, "digits": 2, "Unit": "kVArh", "use": False}, 224 | "Gesamtwirkleistung": 225 | {"port": 84, "digits": 2, "Unit": "W", "use": True}, 226 | "Max_Gesamtwirkleistung": 227 | {"port": 86, "digits": 2, "Unit": "W", "use": True}, 228 | "CurrentSystemPositivePowerDemand": 229 | {"port": 88, "digits": 2, "Unit": "W", "use": True}, 230 | "MaximumSystemPositivePowerDemand": 231 | {"port": 90, "digits": 2, "Unit": "W", "use": True}, 232 | "CurrentSystemReversePowerDemand": 233 | {"port": 92, "digits": 2, "Unit": "W", "use": True}, 234 | "Strom_L1_demand": 235 | {"port": 258, "digits": 2, "Unit": "A", "use": True}, 236 | "Max_Strom_L1_demand": 237 | {"port": 264, "digits": 2, "Unit": "A", "use": True}, 238 | "Total_kwh": 239 | {"port": 342, "digits": 2, "Unit": "kWh", "use": True}, 240 | "Total_kvarh": 241 | {"port": 344, "digits": 2, "Unit": "kVArh", "use": True} 242 | } 243 | 244 | def read_input_values(self, input_register_keys=None): 245 | """ 246 | Read all in self.input_register defined data points and stored the result as float value 247 | into self.data dictionary 248 | :return: self.data dictionary 249 | """ 250 | self.data = {} 251 | if input_register_keys is None: 252 | input_register_keys = self.get_input_keys() 253 | if self.instrument is not None: 254 | for key in input_register_keys: 255 | self.log.debug("try: key='{}', reg='{}', digits='{}'".format(key, self.input_register[key]["port"], 256 | self.input_register[key]["digits"])) 257 | if self.input_register[key]["use"] is True: 258 | 259 | fehler = 0 260 | while True: # Anzahl der Versuche 261 | try: 262 | messwert = self.instrument.read_float(functioncode=4, # fix (!) for this model 263 | registeraddress=self.input_register[key]["port"], 264 | number_of_registers=self.input_register[key][ 265 | "digits"]) 266 | except OSError: 267 | fehler += 1 268 | self.log.error("Kommunikationserror Nr. {}".format(fehler)) 269 | sleep(5) 270 | if fehler > 5: # Anzahl der Versuche 271 | raise OSError 272 | else: 273 | break 274 | 275 | if messwert is None: 276 | self.log.warn("Value '{}' not available".format(key)) 277 | else: 278 | self.data[key] = round(messwert, 4) 279 | self.log.debug("Value '{}' = '{}'".format(key, self.data[key])) 280 | else: 281 | self.log.debug("Value '{}' not used!".format(key)) 282 | pass 283 | else: 284 | err_msg = "No instrument available!" 285 | self.log.error(err_msg) 286 | return None 287 | return self.data 288 | 289 | def get_input_keys(self): 290 | """ 291 | Hilfsmethode zur Erstellung der Intervallklassen 292 | :return: 293 | """ 294 | input_register_keys = [key for key in self.input_register] 295 | return input_register_keys 296 | 297 | 298 | class SDM530(ModBusRTU): 299 | """ 300 | Driver class for energy meter 'SDM530-ModBus' (B+G E-Tech EASTRON) 301 | 302 | Data Format: 4 bytes (2 registers) per parameter. Floating point format ( to IEEE 754) 303 | Most significant register first (Default). 304 | The default may be changed if required -See Holding Register "Register Order" parameter. 305 | """ 306 | def __init__(self, logger, serial_if, serial_if_baud, serial_if_byte, 307 | serial_if_par, serial_if_stop, slave_addr, timeout): 308 | super().__init__(logger, serial_if, serial_if_baud, serial_if_byte, 309 | serial_if_par, serial_if_stop, slave_addr, timeout) 310 | # Konfiguration der Input Register nach Datenblatt 311 | self.input_register = { 312 | "Spannung_L1": { 313 | "port": 0, "digits": 2, "Unit": "V", "use": True}, 314 | "Spannung_L2": { 315 | "port": 2, "digits": 2, "Unit": "V", "use": True}, 316 | "Spannung_L3": { 317 | "port": 4, "digits": 2, "Unit": "V", "use": True}, 318 | "Strom_L1": { 319 | "port": 6, "digits": 2, "Unit": "A", "use": True}, 320 | "Strom_L2": { 321 | "port": 8, "digits": 2, "Unit": "A", "use": True}, 322 | "Strom_L3": { 323 | "port": 10, "digits": 2, "Unit": "A", "use": True}, 324 | "Wirkleistung_L1": { 325 | "port": 12, "digits": 2, "Unit": "W", "use": True}, 326 | "Wirkleistung_L2": { 327 | "port": 14, "digits": 2, "Unit": "W", "use": True}, 328 | "Wirkleistung_L3": { 329 | "port": 16, "digits": 2, "Unit": "W", "use": True}, 330 | "Scheinleistung_L1": { 331 | "port": 18, "digits": 2, "Unit": "VA", "use": True}, 332 | "Scheinleistung_L2": { 333 | "port": 20, "digits": 2, "Unit": "VA", "use": True}, 334 | "Scheinleistung_L3": { 335 | "port": 22, "digits": 2, "Unit": "VA", "use": True}, 336 | "Blindleistung_L1": { 337 | "port": 24, "digits": 2, "Unit": "VAr", "use": True}, 338 | "Blindleistung_L2": { 339 | "port": 26, "digits": 2, "Unit": "VAr", "use": True}, 340 | "Blindleistung_L3": { 341 | "port": 28, "digits": 2, "Unit": "VAr", "use": True}, 342 | "Leistungsfaktor_L1": { 343 | "port": 30, "digits": 2, "Unit": "", "use": True}, 344 | "Leistungsfaktor_L2": { 345 | "port": 32, "digits": 2, "Unit": "", "use": True}, 346 | "Leistungsfaktor_L3": { 347 | "port": 34, "digits": 2, "Unit": "", "use": True}, 348 | "Phasenwinkel_L1": { 349 | "port": 36, "digits": 2, "Unit": "Grad", "use": True}, 350 | "Phasenwinkel_L2": { 351 | "port": 38, "digits": 2, "Unit": "Grad", "use": True}, 352 | "Phasenwinkel_L3": { 353 | "port": 40, "digits": 2, "Unit": "Grad", "use": True}, 354 | "Durchschnittliche_Spannung_zu_N": { 355 | "port": 42, "digits": 2, "Unit": "V", "use": True}, 356 | "Durchschnittlicher_Strom_zu_N": { 357 | "port": 46, "digits": 2, "Unit": "A", "use": True}, 358 | "aktueller_Gesamtstrom": { 359 | "port": 48, "digits": 2, "Unit": "A", "use": True}, 360 | "aktuelle_Gesamtwirkleistung": { 361 | "port": 52, "digits": 2, "Unit": "W", "use": True}, 362 | "aktuelle_Gesamtscheinleistung": { 363 | "port": 56, "digits": 2, "Unit": "VA", "use": True}, 364 | "aktuelle_Gesamtblindleistung": { 365 | "port": 60, "digits": 2, "Unit": "VAr", "use": True}, 366 | "aktueller_Gesamtleistungsfaktor": { 367 | "port": 62, "digits": 2, "Unit": "", "use": True}, 368 | "aktueller_Gesamtphasenwinkel": { 369 | "port": 66, "digits": 2, "Unit": "A", "use": True}, 370 | "Frequenz": { 371 | "port": 70, "digits": 2, "Unit": "Hz", "use": True}, 372 | "Import_Wh_seit_reset": { 373 | "port": 72, "digits": 2, "Unit": "kWh", "use": True}, 374 | "Export_Wh_seit_reset": { 375 | "port": 74, "digits": 2, "Unit": "kWH", "use": True}, 376 | "Import_VArh_seit_reset": { 377 | "port": 76, "digits": 2, "Unit": "kVArh", "use": True}, 378 | "Export_VArh_seit_reset": { 379 | "port": 78, "digits": 2, "Unit": "kVArh", "use": True}, 380 | "VAh_seit_reset": { 381 | "port": 80, "digits": 2, "Unit": "kVAh", "use": True}, 382 | "Ah_seit_reset": { 383 | "port": 82, "digits": 2, "Unit": "Ah", "use": True}, 384 | "Gesamtwirkleistung": { 385 | "port": 84, "digits": 2, "Unit": "W", "use": True}, 386 | "Max_Gesamtwirkleistung": { 387 | "port": 86, "digits": 2, "Unit": "W", "use": True}, 388 | "Gesamtscheinleistung": { 389 | "port": 100, "digits": 2, "Unit": "VA", "use": True}, 390 | "Max_Gesamtscheinleistung": { 391 | "port": 102, "digits": 2, "Unit": "VA", "use": True}, 392 | "Gesamtstrom_Neutralleiter": { 393 | "port": 104, "digits": 2, "Unit": "A", "use": True}, 394 | "Max_Strom_Neutralleiter": { 395 | "port": 106, "digits": 2, "Unit": "A", "use": True}, 396 | "Spannung_L1_L2": { 397 | "port": 200, "digits": 2, "Unit": "V", "use": True}, 398 | "Spannung_L2_L3": { 399 | "port": 202, "digits": 2, "Unit": "V", "use": True}, 400 | "Spannung_L3_L1": { 401 | "port": 204, "digits": 2, "Unit": "V", "use": True}, 402 | "Durchschnittsspannung_L_L": { 403 | "port": 206, "digits": 2, "Unit": "V", "use": True}, 404 | "Strom_Neutralleiter": { 405 | "port": 224, "digits": 2, "Unit": "A", "use": True}, 406 | "THD_Spannung_L1": { 407 | "port": 234, "digits": 2, "Unit": "%", "use": True}, 408 | "THD_Spannung_L2": { 409 | "port": 236, "digits": 2, "Unit": "%", "use": True}, 410 | "THD_Spannung_L3": { 411 | "port": 238, "digits": 2, "Unit": "%", "use": True}, 412 | "THD_Strom_L1": { 413 | "port": 240, "digits": 2, "Unit": "%", "use": True}, 414 | "THD_Strom_L2": { 415 | "port": 242, "digits": 2, "Unit": "%", "use": True}, 416 | "THD_Strom_L3": { 417 | "port": 244, "digits": 2, "Unit": "%", "use": True}, 418 | "THD_Durchschnittliche_Spannung_zu_N": { 419 | "port": 248, "digits": 2, "Unit": "%", "use": True}, 420 | "THD_Durchschnittlicher_Strom_zu_N": { 421 | "port": 250, "digits": 2, "Unit": "%", "use": True}, 422 | "Gesamtsystemleistungsfaktor": { 423 | "port": 254, "digits": 2, "Unit": "Grad", "use": True}, 424 | "Strom_L1_demand": { 425 | "port": 258, "digits": 2, "Unit": "A", "use": True}, 426 | "Strom_L2_demand": { 427 | "port": 260, "digits": 2, "Unit": "A", "use": True}, 428 | "Strom_L3_demand": { 429 | "port": 262, "digits": 2, "Unit": "A", "use": True}, 430 | "Max_Strom_L1_demand": { 431 | "port": 264, "digits": 2, "Unit": "A", "use": True}, 432 | "Max_Strom_L2_demand": { 433 | "port": 266, "digits": 2, "Unit": "A", "use": True}, 434 | "Max_Strom_L3_demand": { 435 | "port": 268, "digits": 2, "Unit": "A", "use": True}, 436 | "THD_Spannung_L1_L2": { 437 | "port": 334, "digits": 2, "Unit": "%", "use": True}, 438 | "THD_Spannung_L2_L3": { 439 | "port": 336, "digits": 2, "Unit": "%", "use": True}, 440 | "THD_Spannung_L3_L1": { 441 | "port": 338, "digits": 2, "Unit": "%", "use": True}, 442 | "THD_Durchschnittliche_Spannung_zu_L_L": { 443 | "port": 340, "digits": 2, "Unit": "%", "use": True}, 444 | "Total_kwh": { 445 | "port": 342, "digits": 2, "Unit": "kwh", "use": True}, 446 | "Total_kvarh": { 447 | "port": 344, "digits": 2, "Unit": "kvarh", "use": True} 448 | } 449 | 450 | # Konfiguration der Holding Register nach Datenblatt 451 | # TODO: Holding Register schreiben 452 | self.holding_register = {} 453 | 454 | def read_input_values(self, input_register_keys=None): 455 | """ 456 | Read in self.input_register defined data points and stored the result as float value into self.data 457 | dictionary 458 | :return: self.data dictionary 459 | """ 460 | self.data = {} 461 | if input_register_keys is None: 462 | input_register_keys = self.get_input_keys() 463 | if self.instrument is not None: 464 | for key in input_register_keys: 465 | self.log.debug("try: key='{}', reg='{}', digits='{}'".format(key, self.input_register[key]["port"], 466 | self.input_register[key]["digits"])) 467 | if self.input_register[key]["use"] is True: 468 | 469 | fehler = 0 470 | while True: # Anzahl der Versuche 471 | try: 472 | messwert = self.instrument.read_float(functioncode=4, # fix (!) for this model 473 | registeraddress=self.input_register[key]["port"], 474 | number_of_registers=self.input_register[key][ 475 | "digits"]) 476 | except OSError: 477 | fehler += 1 478 | self.log.error("Kommunikationserror Nr. {}".format(fehler)) 479 | sleep(5) 480 | if fehler > 5: # Anzahl der Versuche 481 | raise OSError 482 | else: 483 | break 484 | 485 | if messwert is None: 486 | self.log.warn("Value '{}' not available".format(key)) 487 | else: 488 | self.data[key] = round(messwert, 4) 489 | self.log.debug("Value '{}' = '{}'".format(key, self.data[key])) 490 | else: 491 | self.log.debug("Value '{}' not used!".format(key)) 492 | pass 493 | else: 494 | err_msg = "No instrument available!" 495 | self.log.error(err_msg) 496 | return None 497 | return self.data 498 | 499 | def get_input_keys(self): 500 | """ 501 | Hilfsmethode zur Erstellung der Intervallklassen 502 | :return: 503 | """ 504 | input_register_keys = [key for key in self.input_register] 505 | return input_register_keys 506 | 507 | 508 | class SDM630(ModBusRTU): 509 | """ 510 | NEUES Modell muss angepasst werden ******** 511 | Driver class for energy meter 'SDM630-ModBus' (B+G E-Tech EASTRON) 512 | 513 | Data Format: 4 bytes (2 registers) per parameter. Floating point format ( to IEEE 754) 514 | Most significant register first (Default). 515 | The default may be changed if required -See Holding Register "Register Order" parameter. 516 | """ 517 | 518 | def __init__(self, logger, serial_if, serial_if_baud, serial_if_byte, 519 | serial_if_par, serial_if_stop, slave_addr, timeout): 520 | super().__init__(logger, serial_if, serial_if_baud, serial_if_byte, 521 | serial_if_par, serial_if_stop, slave_addr, timeout) 522 | # Konfiguration der Input Register nach Datenblatt 523 | self.input_register = { 524 | "Spannung_L1": { 525 | "port": 0, "digits": 2, "Unit": "V", "use": True}, 526 | "Spannung_L2": { 527 | "port": 2, "digits": 2, "Unit": "V", "use": True}, 528 | "Spannung_L3": { 529 | "port": 4, "digits": 2, "Unit": "V", "use": True}, 530 | "Strom_L1": { 531 | "port": 6, "digits": 2, "Unit": "A", "use": True}, 532 | "Strom_L2": { 533 | "port": 8, "digits": 2, "Unit": "A", "use": True}, 534 | "Strom_L3": { 535 | "port": 10, "digits": 2, "Unit": "A", "use": True}, 536 | "Wirkleistung_L1": { 537 | "port": 12, "digits": 2, "Unit": "W", "use": True}, 538 | "Wirkleistung_L2": { 539 | "port": 14, "digits": 2, "Unit": "W", "use": True}, 540 | "Wirkleistung_L3": { 541 | "port": 16, "digits": 2, "Unit": "W", "use": True}, 542 | "Scheinleistung_L1": { 543 | "port": 18, "digits": 2, "Unit": "VA", "use": True}, 544 | "Scheinleistung_L2": { 545 | "port": 20, "digits": 2, "Unit": "VA", "use": True}, 546 | "Scheinleistung_L3": { 547 | "port": 22, "digits": 2, "Unit": "VA", "use": True}, 548 | "Blindleistung_L1": { 549 | "port": 24, "digits": 2, "Unit": "VAr", "use": True}, 550 | "Blindleistung_L2": { 551 | "port": 26, "digits": 2, "Unit": "VAr", "use": True}, 552 | "Blindleistung_L3": { 553 | "port": 28, "digits": 2, "Unit": "VAr", "use": True}, 554 | "Leistungsfaktor_L1": { 555 | "port": 30, "digits": 2, "Unit": "", "use": True}, 556 | "Leistungsfaktor_L2": { 557 | "port": 32, "digits": 2, "Unit": "", "use": True}, 558 | "Leistungsfaktor_L3": { 559 | "port": 34, "digits": 2, "Unit": "", "use": True}, 560 | "Phasenwinkel_L1": { 561 | "port": 36, "digits": 2, "Unit": "Grad", "use": True}, 562 | "Phasenwinkel_L2": { 563 | "port": 38, "digits": 2, "Unit": "Grad", "use": True}, 564 | "Phasenwinkel_L3": { 565 | "port": 40, "digits": 2, "Unit": "Grad", "use": True}, 566 | "Durchschnittliche_Spannung_zu_N": { 567 | "port": 42, "digits": 2, "Unit": "V", "use": True}, 568 | "Durchschnittlicher_Strom_zu_N": { 569 | "port": 46, "digits": 2, "Unit": "A", "use": True}, 570 | "aktueller_Gesamtstrom": { 571 | "port": 48, "digits": 2, "Unit": "A", "use": True}, 572 | "aktuelle_Gesamtwirkleistung": { 573 | "port": 52, "digits": 2, "Unit": "W", "use": True}, 574 | "aktuelle_Gesamtscheinleistung": { 575 | "port": 56, "digits": 2, "Unit": "VA", "use": True}, 576 | "aktuelle_Gesamtblindleistung": { 577 | "port": 60, "digits": 2, "Unit": "VAr", "use": True}, 578 | "aktueller_Gesamtleistungsfaktor": { 579 | "port": 62, "digits": 2, "Unit": "", "use": True}, 580 | "aktueller_Gesamtphasenwinkel": { 581 | "port": 66, "digits": 2, "Unit": "A", "use": True}, 582 | "Frequenz": { 583 | "port": 70, "digits": 2, "Unit": "Hz", "use": True}, 584 | "Import_Wh_seit_reset": { 585 | "port": 72, "digits": 2, "Unit": "kWh", "use": True}, 586 | "Export_Wh_seit_reset": { 587 | "port": 74, "digits": 2, "Unit": "kWH", "use": True}, 588 | "Import_VArh_seit_reset": { 589 | "port": 76, "digits": 2, "Unit": "kVArh", "use": True}, 590 | "Export_VArh_seit_reset": { 591 | "port": 78, "digits": 2, "Unit": "kVArh", "use": True}, 592 | "VAh_seit_reset": { 593 | "port": 80, "digits": 2, "Unit": "kVAh", "use": True}, 594 | "Ah_seit_reset": { 595 | "port": 82, "digits": 2, "Unit": "Ah", "use": True}, 596 | "Gesamtwirkleistung": { 597 | "port": 84, "digits": 2, "Unit": "W", "use": True}, 598 | "Max_Gesamtwirkleistung": { 599 | "port": 86, "digits": 2, "Unit": "W", "use": True}, 600 | "Gesamtscheinleistung": { 601 | "port": 100, "digits": 2, "Unit": "VA", "use": True}, 602 | "Max_Gesamtscheinleistung": { 603 | "port": 102, "digits": 2, "Unit": "VA", "use": True}, 604 | "Gesamtstrom_Neutralleiter": { 605 | "port": 104, "digits": 2, "Unit": "A", "use": True}, 606 | "Max_Strom_Neutralleiter": { 607 | "port": 106, "digits": 2, "Unit": "A", "use": True}, 608 | "Spannung_L1_L2": { 609 | "port": 200, "digits": 2, "Unit": "V", "use": True}, 610 | "Spannung_L2_L3": { 611 | "port": 202, "digits": 2, "Unit": "V", "use": True}, 612 | "Spannung_L3_L1": { 613 | "port": 204, "digits": 2, "Unit": "V", "use": True}, 614 | "Durchschnittsspannung_L_L": { 615 | "port": 206, "digits": 2, "Unit": "V", "use": True}, 616 | "Strom_Neutralleiter": { 617 | "port": 224, "digits": 2, "Unit": "A", "use": True}, 618 | "THD_Spannung_L1": { 619 | "port": 234, "digits": 2, "Unit": "%", "use": True}, 620 | "THD_Spannung_L2": { 621 | "port": 236, "digits": 2, "Unit": "%", "use": True}, 622 | "THD_Spannung_L3": { 623 | "port": 238, "digits": 2, "Unit": "%", "use": True}, 624 | "THD_Strom_L1": { 625 | "port": 240, "digits": 2, "Unit": "%", "use": True}, 626 | "THD_Strom_L2": { 627 | "port": 242, "digits": 2, "Unit": "%", "use": True}, 628 | "THD_Strom_L3": { 629 | "port": 244, "digits": 2, "Unit": "%", "use": True}, 630 | "THD_Durchschnittliche_Spannung_zu_N": { 631 | "port": 248, "digits": 2, "Unit": "%", "use": True}, 632 | "THD_Durchschnittlicher_Strom_zu_N": { 633 | "port": 250, "digits": 2, "Unit": "%", "use": True}, 634 | "Strom_L1_demand": { 635 | "port": 258, "digits": 2, "Unit": "A", "use": True}, 636 | "Strom_L2_demand": { 637 | "port": 260, "digits": 2, "Unit": "A", "use": True}, 638 | "Strom_L3_demand": { 639 | "port": 262, "digits": 2, "Unit": "A", "use": True}, 640 | "Max_Strom_L1_demand": { 641 | "port": 264, "digits": 2, "Unit": "A", "use": True}, 642 | "Max_Strom_L2_demand": { 643 | "port": 266, "digits": 2, "Unit": "A", "use": True}, 644 | "Max_Strom_L3_demand": { 645 | "port": 268, "digits": 2, "Unit": "A", "use": True}, 646 | "THD_Spannung_L1_L2": { 647 | "port": 334, "digits": 2, "Unit": "%", "use": True}, 648 | "THD_Spannung_L2_L3": { 649 | "port": 336, "digits": 2, "Unit": "%", "use": True}, 650 | "THD_Spannung_L3_L1": { 651 | "port": 338, "digits": 2, "Unit": "%", "use": True}, 652 | "THD_Durchschnittliche_Spannung_zu_L_L": { 653 | "port": 340, "digits": 2, "Unit": "%", "use": True}, 654 | "Total_kwh": { 655 | "port": 342, "digits": 2, "Unit": "kwh", "use": True}, 656 | "Total_kvarh": { 657 | "port": 344, "digits": 2, "Unit": "kvarh", "use": True}, 658 | "Import_L1_kwh": { 659 | "port": 346, "digits": 2, "Unit": "kwh", "use": True}, 660 | "Import_L2_kwh": { 661 | "port": 348, "digits": 2, "Unit": "kwh", "use": True}, 662 | "Import_L3_kwh": { 663 | "port": 350, "digits": 2, "Unit": "kwh", "use": True}, 664 | "Export_L1_kwh": { 665 | "port": 352, "digits": 2, "Unit": "kwh", "use": True}, 666 | "Export_L2_kwh": { 667 | "port": 354, "digits": 2, "Unit": "kwh", "use": True}, 668 | "Export_L3_kwh": { 669 | "port": 356, "digits": 2, "Unit": "kwh", "use": True}, 670 | "Gesamtstrom_L1_kwh": { 671 | "port": 358, "digits": 2, "Unit": "kwh", "use": True}, 672 | "Gesamtstrom_L2_kwh": { 673 | "port": 360, "digits": 2, "Unit": "kwh", "use": True}, 674 | "Gesamtstrom_L3_kwh": { 675 | "port": 362, "digits": 2, "Unit": "kwh", "use": True}, 676 | "Import_L1_kvarh": { 677 | "port": 364, "digits": 2, "Unit": "kvarh", "use": True}, 678 | "Import_L2_kvarh": { 679 | "port": 366, "digits": 2, "Unit": "kvarh", "use": True}, 680 | "Import_L3_kvarh": { 681 | "port": 368, "digits": 2, "Unit": "kvarh", "use": True}, 682 | "Export_L1_kvarh": { 683 | "port": 370, "digits": 2, "Unit": "kvarh", "use": True}, 684 | "Export_L2_kvarh": { 685 | "port": 372, "digits": 2, "Unit": "kvarh", "use": True}, 686 | "Export_L3_kvarh": { 687 | "port": 374, "digits": 2, "Unit": "kvarh", "use": True}, 688 | "Total_L1_kvarh": { 689 | "port": 376, "digits": 2, "Unit": "kvarh", "use": True}, 690 | "Total_L2_kvarh": { 691 | "port": 378, "digits": 2, "Unit": "kvarh", "use": True}, 692 | "Total_L3_kvarh": { 693 | "port": 380, "digits": 2, "Unit": "kvarh", "use": True}, 694 | } 695 | 696 | # Konfiguration der Holding Register nach Datenblatt 697 | # TODO: Holding Register schreiben 698 | self.holding_register = {} 699 | 700 | def read_input_values(self, input_register_keys=None): 701 | """ 702 | Read in self.input_register defined data points and stored the result as float value into self.data 703 | dictionary 704 | :return: self.data dictionary 705 | """ 706 | self.data = {} 707 | if input_register_keys is None: 708 | input_register_keys = self.get_input_keys() 709 | if self.instrument is not None: 710 | for key in input_register_keys: 711 | self.log.debug("try: key='{}', reg='{}', digits='{}'".format(key, self.input_register[key]["port"], 712 | self.input_register[key]["digits"])) 713 | if self.input_register[key]["use"] is True: 714 | 715 | fehler = 0 716 | while True: # Anzahl der Versuche 717 | try: 718 | messwert = self.instrument.read_float(functioncode=4, # 3,4,8,16 for SDM630 4 ok? 719 | registeraddress=self.input_register[key]["port"], 720 | number_of_registers=self.input_register[key][ 721 | "digits"]) 722 | except OSError: 723 | fehler += 1 724 | self.log.error("Kommunikationserror Nr. {}".format(fehler)) 725 | sleep(5) 726 | if fehler > 5: # Anzahl der Versuche 727 | raise OSError 728 | else: 729 | break 730 | 731 | if messwert is None: 732 | self.log.warn("Value '{}' not available".format(key)) 733 | else: 734 | self.data[key] = round(messwert, 4) 735 | self.log.debug("Value '{}' = '{}'".format(key, self.data[key])) 736 | else: 737 | self.log.debug("Value '{}' not used!".format(key)) 738 | pass 739 | else: 740 | err_msg = "No instrument available!" 741 | self.log.error(err_msg) 742 | return None 743 | return self.data 744 | 745 | def get_input_keys(self): 746 | """ 747 | Hilfsmethode zur Erstellung der Intervallklassen 748 | :return: 749 | """ 750 | input_register_keys = [key for key in self.input_register] 751 | return input_register_keys 752 | 753 | 754 | def get_device_list(): 755 | device_list = { 756 | "DDS353B": DDS353B, 757 | "SDM72DM": SDM72DM, 758 | "SDM230": SDM230, 759 | "SDM530": SDM530, 760 | "SDM630": SDM630, 761 | } 762 | return device_list 763 | 764 | 765 | # for test or stand alone work 766 | if __name__ == '__main__': 767 | import logging 768 | try: 769 | em = SDM530(logger=logging, 770 | serial_if="/dev/ttyUSB0", 771 | serial_if_baud=38400, 772 | # serial_if_baud=9600, 773 | serial_if_byte=8, 774 | serial_if_par=serial.PARITY_EVEN, 775 | serial_if_stop=1, 776 | slave_addr=1, 777 | timeout=0.6) 778 | 779 | for wert in em.input_register: 780 | curEnergie = em.instrument.read_float(em.input_register[wert]["port"], 4, 2) 781 | print("{} = {}{}".format(wert, curEnergie, em.input_register[wert]["Unit"])) 782 | except KeyboardInterrupt: 783 | print("Zählerstand nicht auslesbar") 784 | --------------------------------------------------------------------------------