Probleme mit Exceptions von python-telegram-bot

Wenn du dir nicht sicher bist, in welchem der anderen Foren du die Frage stellen sollst, dann bist du hier im Forum für allgemeine Fragen sicher richtig.
Antworten
MikeyMikeDO
User
Beiträge: 6
Registriert: Sonntag 17. Januar 2021, 09:21

Hallo zusammen,

ich bin erst seit kurzem mit python unterwegs und programmiere eine "Alarmanlage" für unseren Garten auf einem Raspberry.
Zur Alarmierung nutze ich u.a. den python-telegram-bot.

Obwohl ich dachte das Thema exceptions einigermaßen verstanden zu haben, scheitere ich jedoch an folgendem Problem und komme nicht wirklich weiter:
Sobald der Router im Garten definiert neu rebootet und somit der Raspberry kurzzeitig keinen Internetzugriff hat erhalte ich aus dem Script bzw. durch den python-telegram-bot haufenweise Fehler wie MaxRetryError, TimeoutError "geraised" von urllib3, OS Error weil das Netzwerk nicht erreichbar ist und so weiter.
Das ist bei einem Alarm Script natürlich nicht wirklich sinnvoll. Ich habe diverse Variationen schon ausprobiert. Es greift jedoch kein einziger try/except block.
Ich habe den stark gekürzten Code mal angehangen und würde mich freuen wenn mir hier jemand mal unter die Arme greifen kann, da ich aktuell keinen Ansatz mehr finde.
Oder habe ich evtl. den Code-Aufbau nicht korrekt?

Code: Alles auswählen

import logging
logging.basicConfig(level=logging.INFO,
                    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')

import datetime                         # Importing the datetime library
import time                             # Nur für eignen Time
import RPi.GPIO as GPIO                 # Importing the GPIO library to use the GPIO pins of Raspberry pi
import http.client                      # Benötigt zum URL Aufruf um eine Nachricht auf dem TV anzuzeigen
from contextlib import closing
from time import sleep                  # Importing the time library to provide the delays in program
from telegram.ext import Updater        # Importing the telepot library
import subprocess                       # Zum Ausführen von Ping benötigt
from urllib3 import exceptions

alarm_relay = 20                        # Initializing GPIO´s
alarm_werkzeugraum = 17
alarm_gartenlaube = 27
...

try:
    updater = Updater(token=bot_api_key, use_context=True)
    updater.start_polling()
except MaxRetryError:
    logging.info("!!!!!!!!!!!!!!!!!!!!!MaxRetryError")
except NetworkError:
    logging.info("!!!!!!!!!!!!!!!!!!!!!NetworkError")
except NewConnectionError:
    logging.info("!!!!!!!!!!!!!!!!!!!!!NewConnectionError")
except ConnectTimeoutError:
    logging.info("!!!!!!!!!!!!!!!!!!!!!ConnectTimeoutError")

except Exception:
    logging.info("Es ist eine nicht behandelte Exception aufgetreten ...")


dispatcher = updater.dispatcher

# Telegram Antwortfunktionen
def start(update, context):
    context.bot.send_message(chat_id=update.effective_chat.id, text="I'm a bot, please talk to me!")

......

# Handler aktivieren
from telegram.ext import CommandHandler
start_handler = CommandHandler('start', start)
dispatcher.add_handler(start_handler)


# Diverse Funktionen
def event_sec15(lasttimer15):
    #print(time.time()) # use for testing, comment out or delete for production
    return lasttimer15 + 15 < time.time()


# Anwesenheitserkennung
def check_anwesenheit():
    global away_count
    ergebnis= 0 
   
    # -c bei Linux und -n bei Windows  
    ipcheck = subprocess.call(['ping', '-c', '1', SMARTPHONE_IP])
    
    if ipcheck == 1:
        away_count = away_count + 1
        print("Abwesenheits-Pingzähler: " +str(away_count))

    if ipcheck == 0:
        away_count = 0
        print("Abwesenheit zurückgesetzt: Pingzähler: " +str(away_count))
            
    if away_count > 4:
        ergebnis = 0
    else:
        ergebnis = 1

    return ergebnis


def main():

    global lasttimer15
    global ...
  
    while 1:
                 
        if event_sec15(lasttimer15) == True:
            lasttimer15 = time.time()
            logging.info("TIK 15!")                       
            anwesend = check_anwesenheit()

        ......
            
        if anwesend == 0:
            if state_alarm_werkzeugraum == 0 and state_alarm_werkzeugraum != laststate_alarm_werkzeugraum:
                updater.bot.send_message(chat_id=group_id, text="Alarm: Tür Werkzeugraum wurde geöffnet!")
                laststate_alarm_werkzeugraum = 0
                sleep(1)
        ......
if __name__ == '__main__':
    try:
        main()
    except KeyboardInterrupt: # If CTRL+C is pressed, exit cleanly:
        print("Steuerung-C erkannt. Programm wird beendet ...")
        updater.stop()
        print("Cleaning GPIOs ...")
        GPIO.cleanup() # cleanup all GPIO
        print("Alles beendet!")
        #break

    except Exception:
        logging.info("MAIN- Es ist eine nicht behandelte Exception aufgetreten ...")

logging.info("Aus MAIN() geflogen!!!!!!!!!!!!!!")



Sirius3
User
Beiträge: 18274
Registriert: Sonntag 21. Oktober 2012, 17:20

Fehlerbehandlung bedeutet nicht, dass man eine Meldung ausgibt und dann so weiter macht, als wäre nichts passiert. Um es mit dem Bild einer Alarmanlage zu sagen: Du hast kreuz und Quer Kabel gezogen und als beim Einschalten mal die Sicherung rausgeflogen ist, hast Du einfach ohne Schaltplan weitere Kabel gezogen, so dass beim nächsten mal Licht einschalten die ganze Bude abbrennen könnte.
Genauso braucht auch ein Programm einen Plan. Bei Dir speziell die Frage, wo kann welcher Fehler auftreten und was soll dann passieren. Und dieses muß man dann auch ausführlich testen. Gerade bei Fehlern ist das gar nicht so einfach, weil man Fehler künstlich reproduzierbar erzeugen muß. Dafür schreibt man Klassen, die sich wie die ursprüngliche Verhalten, nur dass absichtlich Fehler produziert werden.
MikeyMikeDO
User
Beiträge: 6
Registriert: Sonntag 17. Januar 2021, 09:21

Hallo,

ja das ist mir durchaus bewusst. Hierbei handelt es sich erstmal nur um einen Test um die Exception überhaupt abfangen zu können. Das ist ja genau das Problem das es nicht ansatzweise funktioniert. Und mal ehrlich, wenn die Internetverbindung ausfällt möchte ich einen "unlimited retry". Kann hier also nur pausieren und wieder probieren.
Wenn jedoch gefühlt 10 Meldungen von DNS Error bis OS NetworkError, RetryError usw. aufpoppen nur weil die Verbindung mal abbricht, finde ich das schon heftig. Was soll man da groß machen als warten wenn keine weitere redudante Signalisierungsmöglichkeit beseht die ich dann alternativ nutzen könnte. Bzw. ist hier ja das Problem das der python-telegram-bot das erste Errorhandling übernimmt und dann nach einer Anzahl Versuche scheinbar den Fehler "raised".

Mit währe schon im ersten Step damit geholfen den Fehler erstmal abzufangen - um dann folglich falls nötig darauf reagieren zu können. Es wird keiner der Texte ausgegeben. Folglich wird die exception nicht erkannt. Lt. Beispiele von dem Bot kann ich auch auf detailliertere exceptions zugreifen wie z. B. Bot wurde gesperrt. Das möchte ich aber erstmal noch garnicht sondern nur diese einfachen Fehler übergehen, damit das Script durchläuft.
Benutzeravatar
__blackjack__
User
Beiträge: 14053
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@MikeyMikeDO: `INFO` ist ein komischer Log-Level für so etwas. Es auch `WARNING`, `ERROR` und `CRITICAL`, sowie die `exception()`-Methode/Funktion gibt? Letzteres sollte man beispielsweise beim behandeln von `Exception` verwenden. Du gibst da einfach "Es ist eine nicht behandelte Exception aufgetreten ..." aus und man weiss weder welche noch wo. So eine Art der Behandlung ist nicht sinnvoll, weil man die Quelle dann nur sehr schwer finden kann. Und für auskommentierte `print()` wo im Kommentar steht, das sie zum testen da stehen ist sind die Level `DEBUG` und `TRACE` gedacht.

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.

Importe stehen am Anfang eines Moduls, damit man sieht wovon das Modul abhängt und welche Namen wo her kommen.

Konstantennamen werden KOMPLETT_GROSS geschrieben. Die `alarm_*`-Variablen sind ja anscheinedn PIN-Nummern und Konstant.

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

Um verstrichene Zeit auszurechnen sollte man `time.monotonic()` statt `time.time()` verwenden.

Vergiss ``global``. Funktionen/Methoden bekommen alles was sie ausser Konstanten benötigen als Argument(e) übergeben. Und sie verändern nicht irgendwelche globalen Zustände. Das ist schwer zu testen und erschwert das Verständnis und die Fehlersuche. Wenn eine Funktion einen Wert verändern soll, dann bekommt sie den Ausgangswert als Argument und gibt den neuen Wert als Rückgabe an den Aufrufer zurück.

In `check_anwesenheit()` ist die Zuweisung von 0 an `ergebnis` am Anfang überflüssig weil der Wert nirgends verwendet wird.

`ipcheck` wäre ein Namen für eine Funktion die eine IP prüft, eher nicht für deren Rückgabewert. Bei der Auswertung sollte man ``elif`` einsetzen, denn es kann ja nur eine Bedingung zutreffen. Andere Werte als 0 und 1 werden nicht berücksichtigt, das würde ich als Programmfehler ansehen. Wirklich sicher kann man sich eigentlich nur bei 0 sein. Jeder andere Wert bedeutet in der Regel, dass das externe Programm nicht ohne Fehler durchgelaufen ist.

Statt `subprocess.call()` sollte man `subprocess.run()` verwenden. Siehe Dokumentation zu `call()`.

Das zusammenstückeln von Zeichenketten und Werten mittels ``+`` und `str()` ist eher BASIC als Python. Dafür gibt es die `format()`-Methode auf Zeichenketten und f-Zeichenkettenliterale.

Python hat einen Datentyp für Wahrheitswerte (`bool`) mit den literalen Werten `True` und `False`. Da sollte man keine Zahlen für missbrauchen. Also beispielsweise ``ergebnis = False`` statt ``ergebnis = 0`` und ``while True:`` statt ``while 1:``.

Und das Ergebnis von einer Bedingung ist bereits ein Wahrheitswert. Wenn man aufgrund dessen einer anderen Variablen einen Wahrheitswert zuweist, dann braucht man das nicht umständlich mit ``if``/``else`` machen:

Code: Alles auswählen

    if away_count > 4:
        ergebnis = False
    else:
        ergebnis = True
    
    # =>
    
    ergebnis = away_count <= 4
Die `check_anwesenheit()`-Funktion würde dann insgesamt so aussehen:

Code: Alles auswählen

def check_anwesenheit(away_count):
    try:
        # -c bei Linux und -n bei Windows
        subprocess.run(["ping", "-c", "1", SMARTPHONE_IP], check=True)
    except subprocess.CalledProcessError:
        away_count += 1
        print(f"Abwesenheits-Pingzähler: {away_count}")
    else:
        away_count = 0
        print(f"Abwesenheit zurückgesetzt: Pingzähler: {away_count}")

    return away_count <= 4, away_count
Vergkleiche bei Wahrheitswerten macht man nicht gegen literale Wahrheitswerte. Das kommt ja eh nur wieder ein Wahrheitswert bei heraus. Entweder der denn man sowieso schon hatte, dann kann man den direkt verwenden, oder dessen Gegenteil, dafür gibt es dann ``not``. Also statt ``if anwesend == 0:`` bezieungsweise ``if anwesend == False:`` würde man ``if not anwesend:`` schreiben.
“Vir, intelligence has nothing to do with politics!” — Londo Mollari
MikeyMikeDO
User
Beiträge: 6
Registriert: Sonntag 17. Januar 2021, 09:21

Vielen Dank für die tollen Tipps!
Das werde ich so umsetzen, abändern und hoffentlich auch angewöhnen. Habe mir schon gedacht das im Code so einiges zu verbessern ist. Mache das erst seit einigen tagen.

Leider bin ich nur mit den exceptions nicht ansatzweise weiter.
Ich habe hier mal die gesamten Fehlermeldungen abgelegt die so ausgeworfen werden wenn die Verbindung abbricht: https://pastebin.com/8Nz4GHnV
Benutzeravatar
__blackjack__
User
Beiträge: 14053
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@MikeyMikeDO: Das ist ja offenbar alles Protokollierung vom Bot und damit erst einmal ”irrelevant” was da Intern alles an Ausnahmen ausgelöst und verarbeitet wird. Wenn Du aus Deinem Programm die ganze wenig sinnvolle Ausnahmebehandlung komplett raus wirfst und dann so etwas passiert, mit welche Ausnahme bricht denn dann *Dein* Programm ab und wo. *Das* ist doch die Stelle an der Du ansetzen musst und dann überlegen musst an welcher Stelle in Deiner Aufrufhierarchie Du diese Ausnahme(n) sinnvoll behandeln kannst und auch wie Du das am besten machst.
“Vir, intelligence has nothing to do with politics!” — Londo Mollari
MikeyMikeDO
User
Beiträge: 6
Registriert: Sonntag 17. Januar 2021, 09:21

Das kann ich leider nicht wirklich feststellen. Ich habe mal alle try/except herausgenommen.
Das ist die gesamte Ausgabe https://pastebin.com/E1ekfPR7.

Im Stackviewer bekomme ich:
NetworkError: urllib3 HTTPError HTTPSConnectionPool(host='api.telegram.org, port=443) Max retries exceeded with url .....
Kann das leider nicht rauskopieren.
Benutzeravatar
__blackjack__
User
Beiträge: 14053
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@MikeyMikeDO: Das ist mir jetzt ehrlich gesagt zu lang um da rauszusuchen was nicht zu den Logging-Ausgaben gehört sondern tatsächlich als Ausnahme von Deinem Code nicht behandelt wird. Trenn mal das Log von den normalen Ausgaben von Deinem Programm, dann dürfte das übersichtlicher werden.
“Vir, intelligence has nothing to do with politics!” — Londo Mollari
MikeyMikeDO
User
Beiträge: 6
Registriert: Sonntag 17. Januar 2021, 09:21

Ich habe die logging Ausgaben gelöscht. Viel weniger ist es dennoch nicht geworden. Der Rest sind nur noch die Exceptions: https://pastebin.com/1K0WKB6L
Mir ist aufgefallen das in Zeile 833 die Shell Zeichen ">>>" auftauchen. Ist dann die Exception die davor ist die Schuldige?!
Benutzeravatar
__blackjack__
User
Beiträge: 14053
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@MikeyMikeDO: Okay, wir haben also einen `telegram.error.NetworkError` der in Zeile 183 in Deiner `main()`-Funktion auftritt. Das wäre dann schon mal etwas um das Du Dich an entsprechender Stelle sinnvoll kümmern müsstest.

(Das mit dem „exception chaining“ ist zwar nett, bei so etwas aber auch echt nervig da drin dann die eigentliche Ausnahme zu finden die einen interessiert. Früher™ war das einfacher. 👴)
“Vir, intelligence has nothing to do with politics!” — Londo Mollari
MikeyMikeDO
User
Beiträge: 6
Registriert: Sonntag 17. Januar 2021, 09:21

Ich habe da einen Try/Except drumgelegt und getestet. Es hat funktioniert! Oh man! Da muss mann erstmal durchsteigen.
Den Rest werde ich erstmal anpassen um das ganze konform zu haben.

Vielen Dank für die Unterstützung!!!
Antworten