Giter Club home page Giter Club logo

smartmeter's Introduction

Smartmeter

Ziel des Projektes:

Stromzähler/Smartmeter via ModBus und Raspberry Pi auslesen, die Werte in einer Datenbank speichern und mit Grafana visualisieren

Vorbereitungen

Benötigte Hardware

  • Verwendeter Stromzähler: SDM530 von bg-etech

Ebenso möglich ist der Typ DDS353B, SDM230, SDM630 (hier Danke für die Integration an rpi-joe aus dem deutschen Raspberry Pi Forum)

  • Raspberry Pi mit Zubehör

  • USB ModBus Adapter: z.B. hier

    Gibts auch billiger, aber da war mir die Wartezeit im Verhältnis zum Preis zu hoch

  • geschirmtes Buskabel (lt. Anleitung vom Stromzähler)

  • 2x 120Ohm 1/4Watt Abschlusswiderstand

Einbau des Stromzählers nur durch Elektrofachpersonal! Angaben ohne Gewähr! Besser nochmals nach Anleitung prüfen

Benötigte Software

  • Python 3.7 oder höher
  • Grafana zur Visualisierung

Unterstützte Datenbanken

Direkte Verbindung zu

  • sqlite3
  • mySQL
  • PostgreSQL (optional mit timescale)

Für PostgreSQL gibt es noch eine weitere Möglichkeit der Datenübertragung:

Postgrest ermöglicht die Datenübertragung über eine Web-API (muss vom Server natürlich bereitgestellt werden)

Benötigte Python Module

  • Toml
  • Peewee
  • serial
  • minimalmodbus

Installation dieser:

Apt Installation erfordert ggf. root Rechte! Paketquellen zuvor updaten. (apt update)

apt install build-essential libssl-dev libffi-dev python3-dev libpq5 git
git clone https://github.com/Hofei90/smartmeter.git /home/pi/smartmeter
cd /home/pi/smartmeter
pip3 install --user -r requirements.txt
git submodule init && git submodule update

Wird als Datenbank mysql/mariadb verwendet, so muss noch folgendes Paket installiert werden

pip3 install --user PyMySQL

Wird als Datenbank PostgreSQL, so muss noch folgendes Paket installiert werden

pip3 install --user psycopg2

Für sqlite3 ist keine weitere Installation notwendig.

Telegram (Optional)

Ist der Telegrambot nicht erwünscht, so muss in der Konfigurationsdatei false eingetragen werden. Aktuell ist es nur möglich, mit dem Bot das Messintervall zu verkürzen.

MQTT (Optional)

Wird eine Datenübertragung an MQTT gewünscht, so muss in der Konfigurationsdatei is_active auf true gesetzt werden und die Konfiguration für MQTT ausgefüllt werden.

Programm einrichten

Konfiguration anpassen

cp smartmeter_cfg_vorlage.toml smartmeter_cfg.toml

Anschließend die smartmeter_cfg.toml anpassen. Konfigurationsdatei muss im selben Ordner wie die Skripte mit dem Namen smartmeter_cfg.toml gespeichert werden Bei der Anpassung sind < > zu entfernen - " " müssen stehen bleiben.

Inbetriebnahme

Erstmaliger Test:

python3 smartmeter.py ausführen

Wenn dieser Erfolgreich verläuft, erhält man folgende Meldung

18.03.2021 10:27:45 INFO: Durchlaufintervall in Config aktualisiert
 Programm wird beendet. Bitte neu starten

Bevor nun das Programm neu gestartet wird, nochmals die Konfigurationsdatei öffnen und die Einträge bei Durchlaufintervall prüfen und den eigenen Wünschen anpassen. Soll ein Parameter nie gemessen werden, so ist der Wert auf false zu stellen. Ansonsten angeben, bei dem wievielten Durchlauf der entsprechenden Wert jeweils ausgelesen und gespeichert werden soll.

Nun das Programm erneut starten, erscheint anschließend keine Fehlermeldung, so kann eine Service Unit für den Autostart erstellt werden.

Service Unit erstellen

Ausführung erfordert root Rechte

nano /etc/systemd/system/smartmeter.service

# Pfad zum speichern: /etc/systemd/system/smartmeter.service
[Unit]
Description=ServiceUnit zum starten des Smartmeters
After=network.target

[Service]
Type=simple
ExecStart=/usr/bin/python3 /home/pi/smartmeter/smartmeter.py
User=pi


[Install]
WantedBy=multi-user.target

systemctl start smartmeter.service

Kontrolle ob Skript nun wieder aktiv ist, wenn ja automatische Ausführung anlegen:

systemctl enable smartmeter.service

Grafana

Die Visualisierung findet in Grafana statt, auf nähere Ausführungen wird hier jedoch verzichtet, natürlich können die Messwerte auch mit anderen Tools visualsiert werden.

smartmeter's People

Contributors

digitalwm avatar hofei90 avatar linusg avatar nrailuj avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

smartmeter's Issues

Datenbankzugriff mit Peewee

Datenbankzugriff mit Peewee umsetzen, um dem Benutzer die Wahlmöglichkeit zwischen sqlite3, MySQL und Postgres anbieten zu können

DRT428DC-MID

Hallo Hofei90!
Ich habe 3 Stück 3-Phasen Zähler DRT428 von B + G im Einsatz.
Frage: kann ich die Benutzen? Und wie kann ich alle drei in Deinem Projekt einbinden?
Bin nicht so fit im Programmieren.
Kannst Du mir ein paar Tips geben? Wäre sehr dankbar!
Gruss
Hanspi

Klasse für B+GE-TECH WS100-1943 Modbus Zähler

Hier meine einfache Klasse für den WS100 Zähler:
`
class WS100(ModBusRTU):
"""
Driver class for energy meter 'WS100'
This energy meter can be deliver only one value: the front displayed energy value.
"""

def __init__(self, logger, serial_if, serial_if_baud, serial_if_byte,
             serial_if_par, serial_if_stop, slave_addr, timeout):
    super().__init__(logger, serial_if, serial_if_baud, serial_if_byte,
                     serial_if_par, serial_if_stop, slave_addr, timeout)
    # Konfiguration der Input Register nach Datenblatt
    self.input_register = {
        "Gesamtwirkleistung": {
            "port": 261, "digits": 0, "Unit": "W", "use": True},
        "Total_kwh": {
            "port": 271, "digits": 2, "Unit": "kWh", "use": True},
    }

def read_input_values(self, input_register_keys=None):
    """
    Read all in self.input_register defined data points and stored the result as float value
    into self.data dictionary
    :return: self.data dictionary
    """
    self.data = {}
    if input_register_keys is None:
        input_register_keys = self.get_input_keys()
    if self.instrument is not None:
        for key in input_register_keys:
            self.log.debug("try: key='{}', reg='{}', digits='{}'".format(key, self.input_register[key]["port"],
                                                                         self.input_register[key]["digits"]))
            if self.input_register[key]["use"] is True:

                fehler = 0
                while True:  # Anzahl der Versuche
                    try:
                        messwert = self.read_data_point_from_meter(func_code=4, 
                                                                   reg_addr=self.input_register[key]["port"],
                                                                   number_of_reg=self.input_register[key]["digits"])
                                                        
                    except OSError:
                        fehler += 1
                        self.log.error("Kommunikationserror Nr. {}".format(fehler))
                        sleep(5)
                        if fehler > 5:  # Anzahl der Versuche
                            raise OSError
                    else:
                        break

                if messwert is None:
                    self.log.warn("Value '{}' not available".format(key))
                else:
                    self.data[key] = messwert
                self.log.debug("Value '{}' = '{}'".format(key, self.data[key]))
            else:
                self.log.debug("Value '{}' not used!".format(key))
                pass
    else:
        err_msg = "No instrument available!"
        self.log.error(err_msg)
        return None
    return self.data

def get_input_keys(self):
    """
    Hilfsmethode zur Erstellung der Intervallklassen
    :return:
    """
    input_register_keys = [key for key in self.input_register]
    return input_register_keys

`
Sehr viel copy&paste und ich habe noch keine Werte über 2 Bytes, das wird noch nicht funktionieren (reiche ich nach). Die Register weichen von der Beschreibung und von den Registern in mbpoll (modbus test-tool) ab, was ich mir noch nicht erklären kann, aber das liegt möglicherweise an der 2-Bytes-Erfassung (statt 4).

Dazu noch am Ende der Datei das Mapping auf WS100 hinzufügen:
def get_device_list(): device_list = { "WS100": WS100,

Wenn erforderlich ein DB model anlegen:
class WS100(BaseModel): ts = peewee.DateTimeField(primary_key=True) gesamtwirkleistung = peewee.FloatField(null=True) total_kwh = peewee.FloatField(null=True)

Schnittstelle initial ist 9600,8,N,1

Multiple Meters

How would be the best way to use multiple meters?
I have connected them via one RS485 Bus. Addresses are set accordingly. The only solution to find (including start scripts) is to copy everything to a new directory and treat every instance as a complete separate program. In grafana it should then be possible to combine the data sets.
I would like to have different .toml-Files where I specify the addresses and select the data to capture (the second one only needs to capture I_1, I_2, I_3 - everything else doesn't bother me on that device).
Do you know an easy way to achieve this?

Syntax error smartmeter.py line 94

Hi!

I get following error:
root@raspberrypi:/home/pi/smartmeter# python3 smartmeter.py
File "smartmeter.py", line 94
self.headers = {f"Authorization": "{user} {token}".format(user=CONFIG["db"]["postgrest"]["user"],
^
SyntaxError: invalid syntax

Burkhard

Requirements lassen Installation nicht zu

Hallo,

ich wollte eben diese lib auf meinem Pi installieren, allerdings bekomm ich eine Fehlermeldung wegen den Requirements:
`Invalid requirement: 'systemd-python~=234'
Traceback (most recent call last):
File "/usr/share/python-wheels/packaging-19.0-py2.py3-none-any.whl/packaging/requirements.py", line 93, in init
req = REQUIREMENT.parseString(requirement_string)
File "/usr/share/python-wheels/pyparsing-2.2.0-py2.py3-none-any.whl/pyparsing.py", line 1632, in parseString
raise exc
File "/usr/share/python-wheels/pyparsing-2.2.0-py2.py3-none-any.whl/pyparsing.py", line 1622, in parseString
loc, tokens = self._parse( instring, 0 )
File "/usr/share/python-wheels/pyparsing-2.2.0-py2.py3-none-any.whl/pyparsing.py", line 1379, in _parseNoCache
loc,tokens = self.parseImpl( instring, preloc, doActions )
File "/usr/share/python-wheels/pyparsing-2.2.0-py2.py3-none-any.whl/pyparsing.py", line 3395, in parseImpl
loc, exprtokens = e._parse( instring, loc, doActions )
File "/usr/share/python-wheels/pyparsing-2.2.0-py2.py3-none-any.whl/pyparsing.py", line 1383, in _parseNoCache
loc,tokens = self.parseImpl( instring, preloc, doActions )
File "/usr/share/python-wheels/pyparsing-2.2.0-py2.py3-none-any.whl/pyparsing.py", line 3183, in parseImpl
raise ParseException(instring, loc, self.errmsg, self)
pyparsing.ParseException: Expected stringEnd (at char 14), (line:1, col:15)

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
File "/usr/lib/python3/dist-packages/pip/_internal/req/constructors.py", line 253, in install_req_from_line
req = Requirement(req)
File "/usr/share/python-wheels/packaging-19.0-py2.py3-none-any.whl/packaging/requirements.py", line 97, in init
requirement_string[e.loc : e.loc + 8], e.msg
pip._vendor.packaging.requirements.InvalidRequirement: Parse error at "'~=234'": Expected stringEnd
`

Bei Google kommt leider nicht wirklich etwas raus, wenn ich die Fehlermeldung da einklopf.
Ich vermute stark, dass es an meinem Pi liegt, aber find gerade keine Lösungsmöglichkeit. Könnte mir bitte jemand auf die Sprünge helfen?

VIelen Dank schon mal und
liebe Grüße

Wenn Server nicht erreichbar hängt sich Skript auf

Ist der Server nicht erreichbar hängt sich das Skript auf, dass keine weiteren Werte mehr erfasst werden.

Apr 01 10:44:15 raspberrypi smartmeter[6423]: INFO: Durchlaufdauer: 0:00:05.844594 Apr 01 10:44:15 raspberrypi smartmeter[6423]: INFO: Messdauer: 0:00:05.732574 Apr 01 10:44:06 raspberrypi smartmeter[6423]: INFO: Durchlaufdauer: 0:00:07.502115 Apr 01 10:44:06 raspberrypi smartmeter[6423]: INFO: DB Dauer schreiben: 0:00:00.110540 Apr 01 10:44:06 raspberrypi python3[6423]: </html> Apr 01 10:44:06 raspberrypi python3[6423]: </body> Apr 01 10:44:06 raspberrypi python3[6423]: <hr><center>nginx</center> Apr 01 10:44:06 raspberrypi python3[6423]: <center><h1>502 Bad Gateway</h1></center> Apr 01 10:44:06 raspberrypi python3[6423]: <body> Apr 01 10:44:06 raspberrypi python3[6423]: <head><title>502 Bad Gateway</title></head> Apr 01 10:44:06 raspberrypi python3[6423]: <html> Apr 01 10:44:06 raspberrypi python3[6423]: 502 Apr 01 10:44:06 raspberrypi python3[6423]: [{"ts": "2020-04-01 10:43:04", "spannung_l1": 231.1787, "spannung_l2": 231.2233, "spannung_l3": 231.3556, "strom_l1": 1.9614, "strom_l2": 1.152, "strom_l3": 0.743 Apr 01 10:44:06 raspberrypi python3[6423]: </html> Apr 01 10:44:06 raspberrypi python3[6423]: </body> Apr 01 10:44:06 raspberrypi python3[6423]: <hr><center>nginx</center> Apr 01 10:44:06 raspberrypi python3[6423]: <center><h1>502 Bad Gateway</h1></center> Apr 01 10:44:06 raspberrypi python3[6423]: <body> Apr 01 10:44:06 raspberrypi python3[6423]: <head><title>502 Bad Gateway</title></head> Apr 01 10:44:06 raspberrypi python3[6423]: <html> Apr 01 10:44:06 raspberrypi python3[6423]: 502

  • Bei Fehler Werte Zwischenspeichern (Auswahl für Pfad und Anzahl schaffen)

  • Verbindung neu initisalisieren

TypeError: convert_daten_to_influxformat() missing 1 required positional argument: 'device'

Es wurde von mir ein neues Device eingepflegt. Wenn der Devicename falsch wäre würde es doch schon in der Config der .toml rausfliegen.

Traceback (most recent call last):
File "smartmeter.py", line 260, in
main()
File "smartmeter.py", line 250, in main
messhandler.schreibe_messwerte(datenbankschnittstelle)
File "smartmeter.py", line 73, in schreibe_messwerte
datenbankschnittstelle.insert_many(self.messwerte_liste)
File "smartmeter.py", line 123, in insert_many
influx_daten = convert_daten_to_influxformat(daten)
TypeError: convert_daten_to_influxformat() missing 1 required positional argument: 'device'

Readme ergänzen

Sammelung von Punkten, welche in der Readme ergänzt werden müssen

  • < > entfernen - " " stehen lassen in Konfigdatei

Change 'ts' to utc

Visualisation in Grafana is much easier if the timestamp is stored in utc and not in the timezone-format. Currently it is stored in the timezone. For me this gives glitches (+ or - the UTC-Offset). I have to work around it by converting the ts field in the query.
It would be much less pain if you change the timeformat either to utc or let the DB do the work - in mysql it would be possible to use "on update current timestamp" or now(). Then the time stamp is always stored in utc and only converted to the local time zone on SELECT.
What do you think? Discussion in Deutsch is ok :)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.