Problem: MQTT payload Weiterverarbeitung

Python auf Einplatinencomputer wie Raspberry Pi, Banana Pi / Python für Micro-Controller
Antworten
DocMac
User
Beiträge: 2
Registriert: Samstag 20. März 2021, 14:18

Hallo zusammen,

habe gerade erst vor ein paar Tagen mit Python und MQTT usw. angefangen, daher bitte nicht gleich motzen wenn es eine dumme Frage ist.
Ich versuche gerade eine Steuerung des Ladestroms meiner Wallbox entsprechend der vorhandenen PV Überschussleistung am Zählerpunkt zu erstellen.
Dabei wird der Wechselrichter bzw. das Smart Energy Meter über PyModbus ausgelesen und der Ladestrom entsprechend eine möglich 1phasigen oder 3phasigen (>4300W) Ladung berechnet. Das funktioniert soweit.
Da ich aber noch ein paar error cases abfragen möchte (-> Schütz für 1ph->3ph klebt (Kontrollschleife -> am Raspi GPIO #24), Ladezustand an der Wallbox (=Auto lädt gerade), Wallbox Fehler Status) muss ich dazu per MQTT die Wallbox Informationen auslesen und im Programm zur Entscheidung was als nächstes getan werden muss heranziehen.
Aber das kriege es einfach nicht hin :x

Die global Variablen werden egal was ich probiere im Main Programm nicht erkannt.
Ich habe auch eine Version mit einem client = mqtt().Client() usw. probiert (also ohne client_error, client_vehicle) ging auch nicht.
Es scheint ich bekomme den jeweiligen msg.payload nicht in den Main code um die If Funktion durchzuführen.
Muss ich ev. die mehr oder wenige While -Schleife in den def on_message reinnehmen?
Aber dann kann ich doch nicht parallel den Payload des anderen topics lesen und verarbeiten?!

Wäre klasse wenn mir hier jemand sagen könnte wo das Problem liegt bzw. wie man das beheben könnte
Markus


Die entsprechenden Codebereiche:

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Modbus Read & MQTT communication & GPIO actions
import math
import pymodbus
from pymodbus.client.sync import ModbusTcpClient
from pymodbus.constants import Endian
from pymodbus.payload import BinaryPayloadDecoder

import paho.mqtt.client as mqtt
import paho.mqtt.subscribe as subscribe

import RPi.GPIO as GPIO
import time

# Define if GPIO is addressed by Boardnumer 1-40 or like here as GPIO ID
GPIO.setmode(GPIO.BCM)

# Define GPIO 23 as output
# Define GPIO 24 as input (from Schuetzloop - closed)
GPIO.setup(23, GPIO.OUT)
GPIO.setup(24, GPIO.IN)

# The callback for when the client receives a CONNACK response from the server.
def on_connect(client, userdata, flags, rc):
print("Connected with result code "+str(rc))

client.subscribe("warp/ABC/evse/state/error_state")
client.subscribe("warp/ABC/evse/state/vehicle_state")

# The callback for when a PUBLISH message is received from the server

def on_message(client, userdata, msg):
print(" Start on_message ")
print(msg.topic+" : " + msg.payload)
if msg.topic == "warp/ABC/evse/state/error_state" :
global WB_error_state
WB_error_state = msg.payload.decode("utf-8")
if msg.topic == "warp/ABC/evse/state/vehicle_state" :
global vehicle_state
vehicle_state = msg.payload.decode("utf-8")

# Code für Def relevante Modbus Register, Data read out, Formatierung
# ...
#


if __name__ == "__main__":

WB_error_state = 0
vehicle_state = 2
client_error = mqtt.Client()
client_error.connect("192.xxx.yyy.zzz", 1883, 60)
client_error.on_connect = on_connect
client_error.on_message = on_message

client_vehicle = mqtt.Client()
client_vehicle.connect("192.xxx.yyy.zzz", 1883, 60)
client_vehicle.on_connect = on_connect
client_vehicle.on_message = on_message

while WB_error_state == 0:
print("Starting Query.....")
# Code für Modbus Aktionen -> PV Leistung, Überschussberechnung und Berechnung curr_1ph, curr_3p
# ...
#
client_error.subscribe("warp/ABC/evse/state/error_state")
if msg.topic == "warp/ABC/evse/state/error_state" :
WB_error_state = msg.payload.decode("utf-8")

client_vehicle.subscribe("warp/ABC/evse/state/vehicle_state")
if msg.topic == "warp/ABC/evse/state/vehicle_state" :
vehicle_state = msg.payload.decode("utf-8")

# Routine für Umschalten Ladestrom und 3ph
if WB_error_state == 0 and vehicle_state == 2 :
client.publish("warp/ABC/evse/stop_charging","", 2)
time.sleep(2)
GPIO.output(23, GPIO.LOW)
if GPIO.input(24): # If all is okay GPIO 24 signal = high
client.publish("warp/ABC/evse/current_limit", curr_3p, 2)
time.sleep(2)
client.publish("warp/ABC/evse/start_charging","", 2)
elif vehicle_state == 3:
client.publish("warp/ABC/evse/current_limit", 0, 2)
client.publish("warp/ABC/evse/stop_charging", "", 2)
time.sleep(2)
else :
client.publish("warp/ABC/evse/current_limit", curr_1p, 2)
client.loop_forever()
print("End")
GPIO.cleanup()
Benutzeravatar
__blackjack__
User
Beiträge: 13006
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@DocMac: Erst einmal Grundsätzlich: Egal was Du versuchst, versuche nicht globale Variablen zu verwenden.

Die ersten beiden Zeilen sehen so nach Python 2 aus. Das sollte man nicht mehr verwenden, das ist seit mehr als einem Jahr mausetot.

``as`` beim Importieren ist zum umbenennen gedacht, `GPIO` wird aber gar nicht umbenannt.

Kommentare sollen dem Leser einen Mehrwert über den Code geben. Faustregel: Kommentare beschreiben nicht *was* der Code macht, denn das steht da bereits als Code, sondern warum er das macht. Sofern das nicht offensichtlich ist. Offensichtlich ist in aller Regel auch was in der Dokumentation von Python und den verwendeten Bibliotheken steht.

Auf Modulebene sollte nur Code stehen der Konstanten, Funktionen, und Klassen definiert. Das Hauptprogramm steht üblicherweise in einer Funktion die `main()` heisst.

Die `GPIO.cleanup()`-Funktion sollte in jedem Fall aufgerufen werden. Auch wenn das Programm mit Strg+C oder durch einen Fehler abbricht.

Magische Zahlen wie Pin-Nummern sollte man am Anfang als Konstanten definieren. Zum einen ist das wesentlich weniger fehleranfällig und aufwändig wenn man die Pinnummern mal ändern will, zum anderen bekommt die dadurch einen Namen an der der Leser erkennen kann was der Pin für das Programm für eine Bedeutung hat.

Namen werden in Python klein_mit_unterstrichen geschrieben. Ausnahmen sind Konstanten (KOMPLETT_GROSS) und Klassen (PascalCase).

Ich bin gerade extrem verwirrt über die Anzahl der Clients und `subscribe()`-Aufrufe. Du erzeugst zwei Clients subscribest bei beiden jeweils ein Topic, verpasst dann aber beiden die selben Handler. Wobei der `on_connect()`-Händler dann auf dem Client noch mal *beide* Topics subscribed, also das auf das er sowieso schon lauscht und das jeweils andere dann doch auch noch. Und die `on_message()` die ja für beide die selbe ist, behandelt dann auch beide Topics. Das macht nicht wirklich Sinn.

Und dann kommt in der Hauptschleife ein weiteres `client` vor das nirgends definiert ist. Und ein `msg` von mir auch nicht klar ist wo *das* denn bitte her kommen soll?

Man muss die Rückruffunktionen auch setzen bevor man entsprechende Aktionen ausführt. Wenn man `on_connect` *nach* dem `connect()`-Aufruf erst an einen Handler bindet, wird der natürlich nicht nachträglich noch aufgerufen.

Für das Kommunizieren mit den Rückruffunktionen kann man sich beispielsweise eine Klasse mit dem Zustand schreiben und die als `userdata` in die Rückrufe übergeben lassen.

Du vergleichst die `*_state`-Werte mit Zahlen, würdest sie im Handler aber an Zeichenketten binden. Damit ist so ein vergleich immer ungleich weil eine Zahl niemals gleich einer Zeichenkette sein kann.

Die magischen Zahlen für die beiden `state`-Variablen sind schlecht verständlich. Da würde man Konstanten für definieren, oder noch besser einen Aufzählungstyp mit dem `enum`-Modul erstellen.

Code: Alles auswählen

import math
import time

import paho.mqtt.client as mqtt
import paho.mqtt.subscribe as subscribe
import pymodbus
from pymodbus.client.sync import ModbusTcpClient
from pymodbus.constants import Endian
from pymodbus.payload import BinaryPayloadDecoder
from RPi import GPIO

CHARGING_PIN = 23
IS_OK_PIN = 24


class State:
    def __init__(self, error, vehicle):
        self.error = error
        self.vehicle = vehicle


def on_connect(client, _userdata, _flags, return_code):
    print(f"Connected with result code {return_code}")
    client.subscribe("warp/ABC/evse/state/error_state")
    client.subscribe("warp/ABC/evse/state/vehicle_state")


def on_message(_client, state, message):
    print("Start on_message")
    print(f"{message.topic} : {message.payload}")
    if message.topic == "warp/ABC/evse/state/error_state":
        state.error = int(message.payload.decode("utf-8"))
    elif message.topic == "warp/ABC/evse/state/vehicle_state":
        state.vehicle = int(message.payload.decode("utf-8"))
    else:
        print(f"Unexpected topic {message.topic!r}.")


# Code für Def relevante Modbus Register, Data read out, Formatierung
# ...


def main():
    try:
        GPIO.setmode(GPIO.BCM)
        GPIO.setup(CHARGING_PIN, GPIO.OUT)
        GPIO.setup(IS_OK_PIN, GPIO.IN)

        state = State(0, 2)

        client = mqtt.Client(userdata=state)
        client.on_connect = on_connect
        client.on_message = on_message
        client.connect("192.xxx.yyy.zzz")

        while state.error == 0:
            print("Starting Query.....")
            # Code für Modbus Aktionen -> PV Leistung, Überschussberechnung und
            # Berechnung curr_1p, curr_3p
            # ...
            #
            curr_1p = ...
            curr_3p = ...

            if state.error == 0 and state.vehicle == 2:
                client.publish("warp/ABC/evse/stop_charging", "", 2)
                time.sleep(2)
                GPIO.output(CHARGING_PIN, GPIO.LOW)
                if GPIO.input(IS_OK_PIN):
                    client.publish("warp/ABC/evse/current_limit", curr_3p, 2)
                    time.sleep(2)
                    client.publish("warp/ABC/evse/start_charging", "", 2)
            elif state.vehicle == 3:
                client.publish("warp/ABC/evse/current_limit", 0, 2)
                client.publish("warp/ABC/evse/stop_charging", "", 2)
                time.sleep(2)
            else:
                client.publish("warp/ABC/evse/current_limit", curr_1p, 2)

            client.loop_forever()
    finally:
        print("End")
        GPIO.cleanup()


if __name__ == "__main__":
    main()
`GPIO` würde ich durch das `gpiozero`-Modul ersetzen. Das hat die deutlich bessere API.
“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
DocMac
User
Beiträge: 2
Registriert: Samstag 20. März 2021, 14:18

Danke für die schnelle Rückmeldung.

Ich nutze Python3 auf dem Raspi.
Die ersten zwei Zeilen waren im Beispiel Code mit dem ich gestartet habe drin und ich dachte das ist eine Standardeinstellung und nötig. Scheint aber nicht so und kann somit weg?!
Das mit gpiozero und GPIO habe ich zwischenzeitlich auch gelesen. Das werde ich gleich mal umheben.

Die Info zum GPIO.clean up() bzw. dann mittels gpiozero wird auch gleich mit aufgenommen - ich hatte zwar die Meldung von GPIO gesehen mich aber erstmal nicht drum gekümmert, da ich das größere Problem beim MQTT gesehen habe. Anhand Deiner Anmerkung denke ich, dass diese Meldung genau wegen dem nicht "gecleanten" Strg+C Abbruch entsteht.

Die verschiedenen client_x Handler hatte ich erzeugt, da ich dachte ich bekomme nur einen Payload pro client - Handler zurück und könnte daher dann keine zwei subscription Payload in einem Durchlauf verarbeiten. Das ist aber offenbar falsch.
Die ganzen Beispiele die ich mir angeschaut haben, haben da nicht entsprechend weitergeholfen.

Und schlußendlich ist der neue client - Handler im Hauptcode noch ein Überbleibsel meiner Trial & Error Programmierung. Der hätte eigentlich auskommentiert sein sollen (ist vom früheren einzelnen client.on_connect & client.on_message Aufruf übrig).

Ich geh den Code mal genau durch aber es erscheint mir zunächst so als wäre ich gar nicht so weit vom Ziel weggewesen...
Hauptsächlich die Payload Abfrage und die Positionierung und Menge der client - Handler war wohl ziemlich daneben.

Danke nochmal
Antworten