DIY Klingel für Haus programmieren - Robustheit

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.
Homer-S
User
Beiträge: 30
Registriert: Freitag 1. Dezember 2017, 22:30

Hallo zusammen,
ich habe mir mit einem RPi und Kamera eine Klingel für unser Haus gebaut.

Was ich mit dem Pythonprogramm vor habe:
wenn es klingelt, soll die Kamera an gehen und 30 sec aufnehmen.
während der Aufnahme wird eine Video-Datei gespeichert und zu Beginn/Mitte/Ende je ein Bild gespeichert.

Nun soll eine Telegram Nachricht an 2 Empfänger versendet werden mit der Klingelbenachrichtigung und den 3 Bildern.
Danach soll die Klingel/Kamera wieder auf neue Signale warten.

Das meiste geht soweit, jedoch bin ich im Programmieren irgendwo beim C64 Basic stehen geblieben und ich fürchte, das Programm ist erstens nicht sehr robust und zweitens, nennen wir es grob, programmiert.

Manchmal bleibt das Programm eingefroren, nach dem hier:

Code: Alles auswählen

http://192.168.100.111:9000/?action=stream: Invalid data found when processing inputeed=0.962x
frame=  732 fps= 24 q=-1.0 Lsize=   33241kB time=00:00:29.24 bitrate=9313.0kbits/s speed=0.96x
video:33238kB audio:0kB subtitle:0kB other streams:0kB global headers:0kB muxing overhead: 0.011344%
Aufzeichnung beendet
Terminated
Klingel-19_05_06_20_39_33.mp4
Darf ich euch bitten, euch das mal anzuschauen und mir Ratschläge für mehr Robustheit zu machen. Z.B. wenn ein fehler auftritt, dass nicht das Programm terminiert wird.
Oder was man geschickter machen kann usw.

Ich hoffe, das ist ok und ihr habt etwas Spass daran mir unter die Arme zu greifen:

Code: Alles auswählen

#!/usr/bin/env python
#coding: utf8

import time
import RPi.GPIO as GPIO
import os
import subprocess
import requests

#GPIO.setwarnings(False)
GPIO.setmode(GPIO.BOARD)

# PIN festlegen an dem das Signal kommt 
optokoppler = 18
relais = 11

#Telegram konfiguration
telegramurl ="https://api.telegram.org/bot"
bottoken = "7000147:AAGAQ0002HzNim00000spp2w5lNY18"
kate = "76000197"
home/pi/cam
erwin = "700000094"
chatroomid1 = kate
chatroomid2 = erwin

# DEV Mode um Verkabelung zu sparen
developermode = 1

# Konfigurationsvariablen
video_start_kommando = 'mjpg_streamer -i "/usr/local/lib/input_uvc.so -d /dev/video0 -n -r 1024x768 -f 24 –q 80" -o "/usr/local/lib/output_http.so -n -w /usr/local/www -p 9000" -o "/usr/local/lib/output_file.so -f /home/pi/cam -d 12000"'

video_stopp_kommando = 'sudo killall mjpg_streamer'

video_aufnahme_kommando = 'ffmpeg -i "http://192.168.100.111:9000/?action=stream" -vcodec copy -an -t 30 -bufsize 2048k -y "/home/pi/cam/Klingel-$(date +"%y_%m_%d_%H_%M_%S").mp4"'

# Pin optokopller als Eingang, relais als Ausgang festlegen
GPIO.setup(optokoppler, GPIO.IN, pull_up_down = GPIO.PUD_UP)
GPIO.setup(relais, GPIO.OUT)


# Klingelrelais generell anschalten
GPIO.output(relais, GPIO.HIGH) 


# Klingelzähler
i = 0
X = 0
# Endlosschleife
while 1:
    # Eingang lesen
    if (GPIO.input(optokoppler) == GPIO.LOW) or (developermode == 1):
        
        # Videoaufzeichnung starten
        i = i + 1
        print("Videoaufzeichnung gestartet")
        subprocess.Popen(video_start_kommando, shell=True)
        subprocess.Popen(video_aufnahme_kommando, shell=True)
        time.sleep(31)
        os.system(video_stopp_kommando)
        print("Aufzeichnung beendet")

        #Telgram Nachricht versenden
        # Send a photo to a chat room (chat room ID retrieved from getUpdates)
        mymessage = "Es hat gerade das " + str(i) + ". mal geklingelt." 
        url = telegramurl + bottoken + "/sendMessage"
        params = { "chat_id": chatroomid1, "text": mymessage }
        r = requests.get(url, params = params)
        result = r.json()
        params = { "chat_id": chatroomid2, "text": mymessage }
        r = requests.get(url, params = params)
        result = r.json()
        x = 0

        while x < 3:
            x = x + 1
            path = ("/home/pi/cam/")
            photofile = os.listdir(path)[x]
            print(photofile)
            url = telegramurl + bottoken + "/sendPhoto"
            params = { "chat_id": chatroomid1 }
            files = { "photo" : open(path + photofile, "rb") }
            r = requests.get(url, params = params, files = files)
            result = r.json()
            params = { "chat_id": chatroomid2 }
            files = { "photo" : open(photofile, "rb") }
            r = requests.get(url, params = params, files = files)
            result = r.json()
            time.sleep(1)
        x = 0
        os.system('sudo rm /home/pi/cam/*.jpg')
Sirius3
User
Beiträge: 17710
Registriert: Sonntag 21. Oktober 2012, 17:20

Konstanten werden in Python KOMPLETT_GROSS geschrieben. Die ganzen sudo-Aufrufe sind überflüssig. In Python gibt es True,
Warum verwendest Du mal subprocess und mal os.system?
Das senden einer Nachricht solltest Du in einer Funktion auslagern, Du hast viel doppelten Code.
`result` sollte auch geprüft werden.
os.listdir garantiert nicht, dass die Dateien immer in der gleichen Reihenfolge kommen. So ist die while-Schleife auch nicht nur unglaublich umständlich, sondern auch noch instabil.
Beim zweiten open (das Du auch irgendwann schließen solltest) fehlt etwas.
Und statt Deine Wohnung mit einem busy-Loop zu heizen, solltest Du sowas wie edge-Detect benutzen.
Homer-S
User
Beiträge: 30
Registriert: Freitag 1. Dezember 2017, 22:30

Hallo Sirius,

Danke für deine Antwort. Vielleicht vorab. Warum ich etwas getan habe, weil ich etwas machen wollte und das dann ergoogelt hatte. Deshalb, bitte um etwas Geduld und Verständnis, ich lerne erst.

Darf ich dich fragen, was du genau meinst?
In Python gibt es True: was muss ich auf True setzen damit ich das sudo spare?

Warum verwendest Du mal subprocess und mal os.system?
Keine Ahnung!

Nachricht solltest Du in einer Funktion auslagern
Muss ich googeln, wie das geht und wie ich die aufrufe.


`result` sollte auch geprüft werden.
wieso?

os.listdir garantiert nicht, dass die Dateien immer in der gleichen Reihenfolge kommen. So ist die while-Schleife auch nicht nur unglaublich umständlich, sondern auch noch instabil.
Hab ich gemerkt, ich weiß nur nicht wie ich es besser mache.

Deine Wohnung mit einem busy-Loop zu heizen, solltest Du sowas wie edge-Detect
Das muss ich wohl auch ers googeln.

anke trotzdem und bitte, wie geschrieben, nicht ärgern, ich versuche es zu lernen.
Benutzeravatar
__blackjack__
User
Beiträge: 13003
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Homer-S: Den Quelltext hast Du aber nicht laufen lassen, denn da steht Unsinn drin der es mit einem `NameError` beendet. `home` ist nicht definiert und diese Zeile macht wie gesagt auch gar keinen Sinn.

Das `True` hat mit dem ``sudo`` nichts zu tun. Du verwendest bei `developermode` eine 1 wo eigentlich `True` oder `False` verwendet werden sollte, denn das ist es ja letztendlich. Du würdest ja an den Namen auch nicht, sagen wir mal 42 binden. Explizit mit `True` oder `False` vergleichen würde man dann übrigens auch nicht, denn da kommt dann ja nur wieder ein Wahrheitswert bei heraus.

Und auch die 1 bei der ``while``-Schleife sollte ein `True` sein.

Die ``sudo``\s sind ganz einfach überflüssig. Sollte nach dem entfernen etwas nicht mehr funktionieren, dann löse das sauber über Rechtevergabe, statt unnötigerweise etwas mit den höchsten Rechten auszuführen.

Warum weisst Du nicht warum Du `os.system()` und das `subprocess`-Modul verwendest? Nach irgend einem Kriterium musst Du doch entschieden haben wann Du das eine und wann das andere verwendest? Und warum verwendest Du `os.system()` nach dem die Dokumentation davon davon abrät und zum `subprocess`-Modul rät?

Bitte nicht nach Sachen googlen die man in der Python-Dokumentation nachlesen kann. Insbesondere sollte man das Tutorial dort einmal durchgearbeitet haben. Da stehen so grundlegende Sachen wie das definieren und aufrufen von Funktionen beschrieben. Im Netz findet man auch viel Müll. Gerade was die Programmierung auf dem Raspi angeht. Da sind viel zu viele Leute unterwegs die keine Ahnung haben, selbst wenn es nur Python ist von dem sie keine Ahnung haben, und noch mehr die kopieren und einfügen und ändern was sie gar nicht verstanden haben. Teilweise von Leuten die das auch schon so gemacht haben. Darunter leidet die Qualität und nicht selten auch die Korrektheit von Code.

Du machst nirgends etwas mit `result` – warum weisst Du diesem Namen dann überhaupt etwas zu? Was erwartest Du denn da? Die API wird da ja irgend ein Ergebnis liefern‽ Das zum Beispiel aussagt ob die Anfrage überhaupt erfolgreich war‽

Statt der ``while``-Schleife mit dem `x` (die eine ``for``-Schleife sein sollte) hätte man einmal `os.listdir()` aufgerufen, die Liste sortiert, und dann eine ``for``-Schleife über eine Liste mit den ersten drei Elementen gemacht. Das sind alles Grundlagen. Wie gesagt, es gibt in der Python-Dokumentation ein Tutorial.

Pfade setzt man nicht mit ``+`` zusammen. Um Pfade zu manipulieren verwendet man entweder die Funktionen in `os.path` oder das `pathlib`-Modul.

Bei `subprocess.Popen()` solltest Du nicht ``shell=True`` verwenden, ausser wenn man wirklich eine dazwischen geschaltete Shell benötigt, was man vermeiden sollte. Sonst fängt man sich die gleichen Probleme ein wegen denen man `os.system()` nicht verwenden sollte.

Die beiden Prozesse solltest Du gezielt über die erstellten `Popen()`-Objekte beenden statt ein externes ``kill`` aufzurufen. Und auch für das löschen von Dateien ist es kompletter Unsinn eine externe Shell aufzurufen nur damit die dann ``rm`` aufrufen kann.

`i` sollte einen besseren Namen bekommen. Niemand errät dass da die Anzahl der Klingelereignisse mit gezählt wird.

`X` wird definiert, aber nirgends benutzt.

`x` wird unnötigerweise nach der ``while``-Schleife auf 0 gesetzt.

Was soll das `my` in `mymessage`? Warum heisst das nicht einfach nur `message`?

Werte und Zeichenketten mit ``+`` und `str()` zusammensetzen ist eher BASIC als Python. Du hattest ja vorgewarnt. ;-) In Python gibt es dafür Zeichenkettenformatierung mit der `format()`-Methode auf Zeichenketten, und ab Python 3.6 f-Zeichenkettenliterale.

Es sollte mit einem ``try``/``finally`` sichergestellt werden das am Programmende `GPIO.cleanup()` aufgerufen wird.
“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
Benutzeravatar
__blackjack__
User
Beiträge: 13003
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Homer-S: Noch ein paar Probleme die mir aufgefallen sind: `ffmpeg` liest die Daten von `mjpeg_streamer` wenn ich das richtig verstanden habe – allerdings werden die Prozesse direkt nacheinander und asynchron gestartet. Wenn `mjpeg_streamer` noch nicht so weit ist Daten auf Port 9000 bereit zu stellen, wird `ffmpeg` abbrechen weil keiner auf dem Port auf Verbindungen wartet. Das robust zu lösen dürfte interessant sein/werden.

Dann sehe ich nicht wo da eigentlich Bilder gespeichert werden‽

Bei dem `mjpg_streamer`-Aufruf ist eine Option ``–q 80`` wobei das Zeichen vor dem ``q`` kein Minuszeichen sondern ein Gedankenstrich ist. Das ist falsch.

Beim senden wird auch nicht sichergestellt, das da Dateinamen von Bildern ausgewählt werden. Denn es gibt in dem Verzeichnis ja auch eine MP4-Datei die von `ffmpeg` dort gespeichert wird.

Einmal setzt Du `path` (falsch) vor den Dateinamen, beim Senden eines Bildes, und mal nicht. Letzteres ist falsch und müsste das Programm auch zum Abbruch bringen.

Das kann im Grunde nie wirklich so gelaufen sein.
“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
Homer-S
User
Beiträge: 30
Registriert: Freitag 1. Dezember 2017, 22:30

Ich habe einige eurer Ratschläge beherzigt und eingearbeitet.
Was ich noch nicht gefunden habe, wie ich den mjpg_streamer prozess "sauber, ohne sudo" beenden kann

Code: Alles auswählen

#!/usr/bin/env python
#coding: utf8

import time
import RPi.GPIO as GPIO
import os
import subprocess
import requests

#GPIO.setwarnings(False)
GPIO.setmode(GPIO.BOARD)

# PIN festlegen an dem das Signal kommt 
optokoppler = 18
relais = 11

# Funktion Message senden

def sendNachricht(*chatroomid):
        """ versendet die Klingelbenachrichtigung """
        for y in chatroomid:
             mymessage = "Es hat gerade das " + str(i) + ". mal geklingelt."
             url = telegramurl + bottoken + "/sendMessage"
             params = { "chat_id": y, "text": mymessage }
             r = requests.get(url, params = params)
             result = r.json()
             print(result)

def sendBild(*chatroomid):
        for z in chatroomid:
             url = telegramurl + bottoken + "/sendPhoto"
             params = { "chat_id": z }
             files = { "photo" : open(path + photofile, "rb") }
             r = requests.get(url, params = params, files = files)
             result = r.json()
             print(result)




#Telegram konfiguration
telegramurl ="https://api.telegram.org/bot"
bottoken = "75000007:AAGAQDXlh6o00000000000000Y18"
kate = "76000097"
erwin = "7300000094"

# DEV Mode um Verkabelung zu sparen
developermode = True

# Konfigurationsvariablen
video_start_kommando = 'mjpg_streamer -i "/usr/local/lib/input_uvc.so -d /dev/video0 -n -r 1024x768 -f 24 -q 80" -o "/usr/local/lib/output_http.so -n -w /usr/local/www -p 9000" -o "/usr/local/lib/output_file.so -f /home/pi/cam -d 12000"'

video_stopp_kommando = 'sudo killall mjpg_streamer'

video_aufnahme_kommando = 'ffmpeg -i "http://192.168.100.111:9000/?action=stream" -vcodec copy -an -t 30 -bufsize 2048k -y "/home/pi/cam/Klingel-$(date +"%y_%m_%d_%H_%M_%S").mp4"'

# Pin optokopller als Eingang, relais als Ausgang festlegen
GPIO.setup(optokoppler, GPIO.IN, pull_up_down = GPIO.PUD_UP)
GPIO.setup(relais, GPIO.OUT)


# Klingelrelais generell anschalten
GPIO.output(relais, GPIO.HIGH) 


# Klingelzähler
i = 0
# Endlosschleife
while True:
    # Eingang lesen
    if (GPIO.input(optokoppler) == GPIO.LOW) or (developermode):
        
        # Videoaufzeichnung starten
        i = i + 1
        print("Videoaufzeichnung gestartet")
        subprocess.Popen(video_start_kommando, shell=True)
        subprocess.Popen(video_aufnahme_kommando, shell=True)
        time.sleep(31)
        os.system(video_stopp_kommando)
        print("Aufzeichnung beendet")
        sendNachricht(kate, erwin)

        x = 0
        while x < 3:
            x = x + 1
            path = ("/home/pi/cam/")
            photofile = os.listdir(path)[x]
            print(photofile)
            sendBild(kate, erwin)
            time.sleep(1)
        x = 0
        os.system('rm /home/pi/cam/*.jpg')
Homer-S
User
Beiträge: 30
Registriert: Freitag 1. Dezember 2017, 22:30

__blackjack__ hat geschrieben: Montag 6. Mai 2019, 23:57 @Homer-S: Noch ein paar Probleme die mir aufgefallen sind: `ffmpeg` liest die Daten von `mjpeg_streamer` wenn ich das richtig verstanden habe – allerdings werden die Prozesse direkt nacheinander und asynchron gestartet. Wenn `mjpeg_streamer` noch nicht so weit ist Daten auf Port 9000 bereit zu stellen, wird `ffmpeg` abbrechen weil keiner auf dem Port auf Verbindungen wartet. Das robust zu lösen dürfte interessant sein/werden.
eigentlich will ich ja, dass ffmpeg erst beginnt, wenn der mjpeg_streamer läuft. Man könnte ja den Stream auch dauernd laufen lassen und dann dem ffmpeg sagen, er soll 30 sec aufnehmen.
Dann sehe ich nicht wo da eigentlich Bilder gespeichert werden‽
Gute Frage, es werden aber definitiv immer 3 Bilder gespeichert. ich frag mich auch schon die ganze Zeit, wo das mit drin ist ...
Bei dem `mjpg_streamer`-Aufruf ist eine Option ``–q 80`` wobei das Zeichen vor dem ``q`` kein Minuszeichen sondern ein Gedankenstrich ist. Das ist falsch.
Sollte nun passen, wobei er nicht gemotzt hat
Beim senden wird auch nicht sichergestellt, das da Dateinamen von Bildern ausgewählt werden. Denn es gibt in dem Verzeichnis ja auch eine MP4-Datei die von `ffmpeg` dort gespeichert wird.
richtig, die mp4 ist auch im Verzeichnis. die sind aber immer hinter den Bildern sortiert. Wie bekomm ich das besser hin?

Einmal setzt Du `path` (falsch) vor den Dateinamen, beim Senden eines Bildes, und mal nicht. Letzteres ist falsch und müsste das Programm auch zum Abbruch bringen.

Das kann im Grunde nie wirklich so gelaufen sein.
Es lief ... / läuft (nun schon gefühlt besser) immer noch, komisch :)
Benutzeravatar
__blackjack__
User
Beiträge: 13003
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Ich zitiere mich mal selbst:
Die beiden Prozesse solltest Du gezielt über die erstellten `Popen()`-Objekte beenden statt ein externes ``kill`` aufzurufen.
Schau Dir mal an was für Methoden diese Objekte haben. Und Du solltest auch bei beiden die `wait()`-Methode aufrufen, damit keine Zombie-Prozesse entstehen können.

Den Streamer permanent laufen lassen, wäre wahrscheinlich am einfachsten. Solange niemand die Bilder anfordert, macht der ja auch nichts ausser auf eine Verbindung zu warten.

`os.listdir()` hat keine garantierte Reihenfolge. Zu sagen dass die MP4-Datei immer hinter den Bildern ”sortiert” wird ist also falsch. Das mag zufällig immer der Fall sein, ist aber eben nicht garantiert. Ich würde da a) gezielt nur Bilder auflisten (`pathlib`- oder `glob`-Modul) und das Ergebnis b) noch sortieren, falls erforderlich. Entweder nach Dateinamen, falls das die richtige Reihenfolge ergibt, oder nach Zeit der letzten Modifikation der Dateien.

Das kann nur gelaufen sein wenn das aktuelle Arbeitsverzeichnis auch `/home/pi/cam/` war.
“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
Homer-S
User
Beiträge: 30
Registriert: Freitag 1. Dezember 2017, 22:30

__blackjack__ hat geschrieben: Montag 6. Mai 2019, 21:43 @Homer-S: Den Quelltext hast Du aber nicht laufen lassen, denn da steht Unsinn drin der es mit einem `NameError` beendet. `home` ist nicht definiert und diese Zeile macht wie gesagt auch gar keinen Sinn.
wie schon geschrieben, es lief sogar trotzdem ...
Das `True` hat mit dem ``sudo`` nichts zu tun. Du verwendest bei `developermode` eine 1 wo eigentlich `True` oder `False` verwendet werden sollte, denn das ist es ja letztendlich. Du würdest ja an den Namen auch nicht, sagen wir mal 42 binden. Explizit mit `True` oder `False` vergleichen würde man dann übrigens auch nicht, denn da kommt dann ja nur wieder ein Wahrheitswert bei heraus.

Und auch die 1 bei der ``while``-Schleife sollte ein `True` sein.
Hab ich verstanden und glaube ich schon umgesetzt
Die ``sudo``\s sind ganz einfach überflüssig. Sollte nach dem entfernen etwas nicht mehr funktionieren, dann löse das sauber über Rechtevergabe, statt unnötigerweise etwas mit den höchsten Rechten auszuführen.

Warum weisst Du nicht warum Du `os.system()` und das `subprocess`-Modul verwendest? Nach irgend einem Kriterium musst Du doch entschieden haben wann Du das eine und wann das andere verwendest? Und warum verwendest Du `os.system()` nach dem die Dokumentation davon davon abrät und zum `subprocess`-Modul rät?
Das muss ich mir im Tutorial mal noch besser anschauen. Vielleicht kann ich dann ohne das os.system auskommen

Bitte nicht nach Sachen googlen die man in der Python-Dokumentation nachlesen kann. Insbesondere sollte man das Tutorial dort einmal durchgearbeitet haben. Da stehen so grundlegende Sachen wie das definieren und aufrufen von Funktionen beschrieben. Im Netz findet man auch viel Müll. Gerade was die Programmierung auf dem Raspi angeht. Da sind viel zu viele Leute unterwegs die keine Ahnung haben, selbst wenn es nur Python ist von dem sie keine Ahnung haben, und noch mehr die kopieren und einfügen und ändern was sie gar nicht verstanden haben. Teilweise von Leuten die das auch schon so gemacht haben. Darunter leidet die Qualität und nicht selten auch die Korrektheit von Code.
Da triffst du den Nagel auf den Kopf, siehe mein Vorgehen :)
Du machst nirgends etwas mit `result` – warum weisst Du diesem Namen dann überhaupt etwas zu? Was erwartest Du denn da? Die API wird da ja irgend ein Ergebnis liefern‽ Das zum Beispiel aussagt ob die Anfrage überhaupt erfolgreich war‽
hatte das print vergessen
Statt der ``while``-Schleife mit dem `x` (die eine ``for``-Schleife sein sollte) hätte man einmal `os.listdir()` aufgerufen, die Liste sortiert, und dann eine ``for``-Schleife über eine Liste mit den ersten drei Elementen gemacht. Das sind alles Grundlagen. Wie gesagt, es gibt in der Python-Dokumentation ein Tutorial.
Das meinte ich vorhin. Die Bilder sind im Verzeichnis sowieso schon vor den mp4 und da ich die nach dem senden lösche, müssten es eh immer nur max 3 sein.
was wäre/ist der Vorteil zw while und for ?
Pfade setzt man nicht mit ``+`` zusammen. Um Pfade zu manipulieren verwendet man entweder die Funktionen in `os.path` oder das `pathlib`-Modul.
sorry, muss ich noch anschauen
Bei `subprocess.Popen()` solltest Du nicht ``shell=True`` verwenden, ausser wenn man wirklich eine dazwischen geschaltete Shell benötigt, was man vermeiden sollte. Sonst fängt man sich die gleichen Probleme ein wegen denen man `os.system()` nicht verwenden sollte.

Die beiden Prozesse solltest Du gezielt über die erstellten `Popen()`-Objekte beenden statt ein externes ``kill`` aufzurufen. Und auch für das löschen von Dateien ist es kompletter Unsinn eine externe Shell aufzurufen nur damit die dann ``rm`` aufrufen kann.
ahh, das hatte ich vorhin überlesen. muss ich noch schauen wie ich das schreibe ohne das kill ...
Danke
`i` sollte einen besseren Namen bekommen. Niemand errät dass da die Anzahl der Klingelereignisse mit gezählt wird.
`X` wird definiert, aber nirgends benutzt.
`x` wird unnötigerweise nach der ``while``-Schleife auf 0 gesetzt.
Was soll das `my` in `mymessage`? Warum heisst das nicht einfach nur `message`?
hab ich soeben eingearbeitet, ist oben noch nicht drin
Werte und Zeichenketten mit ``+`` und `str()` zusammensetzen ist eher BASIC als Python. Du hattest ja vorgewarnt. ;-) In Python gibt es dafür Zeichenkettenformatierung mit der `format()`-Methode auf Zeichenketten, und ab Python 3.6 f-Zeichenkettenliterale.
muss ich noch nachlesen

Es sollte mit einem ``try``/``finally`` sichergestellt werden das am Programmende `GPIO.cleanup()` aufgerufen wird.
hmm, das hab ich nicht so ganz verstanden, denn eigentlich soll das Programm ja immer laufen ....
Benutzeravatar
__blackjack__
User
Beiträge: 13003
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Das Programm soll immer laufen, aber das tut es ja nicht immer. Wenn das mit einer Ausnahme abbricht, oder wenn Du das selbst mit Strg+C abbrichst, dann sollte halt aufgeräumt werden.
“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
Benutzeravatar
__blackjack__
User
Beiträge: 13003
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Homer-S: Die beiden `send*`-Funktionen haben Namen bei denen im Namen Deutsch und Englisch gemischt sind. Man würde da eher zu komplett englisch tendieren. Zudem ist die Schreibweise falsch: statt der Grossbuchstaben gehört da ein Unterstrich hin.

``*``-”Magie” bei Funktionsdefinitionen ist in der Regel keine gute Idee. Wenn Du da eine Liste mit IDs übergeben willst, dann mach das explizit als Liste bei der Übergabe.

Warum heissen die Laufvariablen in den beiden Funktionen `y` und `z`? Da weiss doch dann keiner was gemeint ist. Die sollten beide `chatroom_id` heissen. Dann fällt auch auf, dass das Funktionsargument falsch benannt ist: das ist ja nicht *eine* ID sondern (potentiell) mehrere IDs, sollte also auch in der Mehrzahl benannt werden.

Die beiden Funktionen haben auch recht viel gemeinsam, so das es sich anbieten würde diese Gemeinsamkeiten in eine weitere Funktion heraus zu ziehen.
“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
Sirius3
User
Beiträge: 17710
Registriert: Sonntag 21. Oktober 2012, 17:20

@Homer-S: die neuen Funktionen sendNachricht und sendBild benutzen eine Vielzahl globaler Variablen. Alles was eine Funktion braucht, muß sie auch über ihre Argumente bekommen. `y` und `z` sind auch besonders schlechte Namen für chat-IDs, vor allem weil in zwei Funktionen für den selben Inhalt verschiedene Namen verwendet werden!

Ansonsten ist der Code noch sehr überarbeitungsbedürftig.
Das Zusammenstückeln von Strings mit + ist immer noch drin, eingerückt wird einheitlich mit 4 Leerzeichen pro Ebene, Konstanten schreibt man komplett groß: OPTOKOPLER, TELEGRAM_URL, etc. Variablen und Funktionen schreibt man klein mit Unterstrich send_nachricht. Konstanten gehören auch an den Anfang der Datei.
Alles ab Zeile 58 sollte in eine Funktion wandern, so dass Du auch nicht mehr aus Versehen globale Variablen benutzen kannst.
Statt busy-loop wait_for_edge benutzen.
Die kill-Methode von subprocess.Popen benutzen.
Statt das listdir-Konstrukt etwas stabiles benutzen.
Statt rm die passenden Python-Funktion benutzen.

Ein Zwischenschritt könnte so aussehen:

Code: Alles auswählen

#!/usr/bin/env python
#coding: utf8

import time
import os
import subprocess
import datetime
from itertools import count
import requests
import RPi.GPIO as gpio

DEVELOPERMODE = True

# PIN festlegen an dem das Signal kommt 
OPTOKOPPLER = 18
RELAIS = 11

#Telegram konfiguration
TELEGRAM_URL ="https://api.telegram.org/bot"
BOT_TOKEN = "75000007:AAGAQDXlh6o00000000000000Y18"
CHAT_IDS = ["76000097", "7300000094"]

BASE_PATH = '/home/pi/cam'

VIDEO_START_COMMAND = ['mjpg_streamer',
    '-i', "/usr/local/lib/input_uvc.so -d /dev/video0 -n -r 1024x768 -f 24 -q 80"
    '-o', "/usr/local/lib/output_http.so -n -w /usr/local/www -p 9000",
    '-o', "/usr/local/lib/output_file.so -f /home/pi/cam -d 12000"]
VIDOE_CONVERT_COMMAND = ['ffmpeg',
    '-i', "http://192.168.100.111:9000/?action=stream",
    '-vcodec', 'copy', '-an', '-t', '30', '-bufsize', '2048k',
    '-y']


def send_message(chatroomids, message=None, photo=None):
    """ versendet die Klingelbenachrichtigung """
    files = {}
    params = {"chat_id": id}
    if message:
        params["text"] = message
        cmd = "sendMessage"
    if photo:
        files = {"photo": photo}
        cmd = "sendPhoto"
    url = telegramurl + bottoken + "{}{}/{}".format(TELEGRAM_URL, BOT_TOKEN, cmd)
    for id in chatroomids:
        r = requests.get(url, params=params, files=files)
        result = r.json()
        print(result)

def setup():
    # Pin optokopller als Eingang, relais als Ausgang festlegen
    gpio.setmode(gpio.BOARD)
    gpio.setup(OPTOKOPPLER, gpio.IN, pull_up_down = gpio.PUD_UP)
    gpio.setup(RELAIS, gpio.OUT)
    # Klingelrelais generell anschalten
    gpio.output(RELAIS, gpio.HIGH) 

def record(ring_count):
    print("Videoaufzeichnung gestartet")
    filename = os.join(BASE_PATH, "Klingel-{:%Y%m%d_%H%M%S}.mp4".format(datetime.datetime.now())
    video = subprocess.Popen(VIDEO_START_COMMAND)
    convert = subprocess.Popen(VIDOE_CONVERT_COMMAND + [filename])
    time.sleep(31)
    video.kill()
    convert.wait()
    print("Aufzeichnung beendet")
    message = "Es hat gerade das {}. mal geklingelt.".format(ring_count)
    send_message(CHAT_IDS, message=message):
    for filename in sorted(glob.glob(os.path.join(BASE_PATH, '*.jpg'))):
        with open(filename, 'rb') as data:
            photo = data.read()
        send_message(CHAT_IDS, photo=photo)
        time.sleep(1)
        os.unlink(filename)

def main():
    try:
        setup()
        for ring_count in count():
            if DEVELOPERMODE or gpio.wait_for_edge(OPTOKOPPLER), gpio.FALLING):
                record(ring_count)
    finally:
        gpio.cleanup()

if __name__ == '__main__':
    main()
Benutzeravatar
__blackjack__
User
Beiträge: 13003
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Warum die Leute immer so furchtbar brutal sein müssen :-) – statt `kill()` was wirklich ein `SIGKILL` an den Kernel sendet, sollte man besser `terminate()` verwenden, was ein `SIGTERM` an den Prozess sendet und dann auch ein `wait()` für `video`.
“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
Homer-S
User
Beiträge: 30
Registriert: Freitag 1. Dezember 2017, 22:30

BOH!!!!

Auf diese "Version" wäre ich alleine nie gekommen.
Ich schau mir die mal im Detail gut durch und probiere es.
Homer-S
User
Beiträge: 30
Registriert: Freitag 1. Dezember 2017, 22:30

Ich habe mir das gut angeschaut, auch einiges gegoogelt, um nicht dumme, selbst erklärbare Fragen zu stellen und dann den Code ausprobiert.
Ich bekomme einen Fehler, den ich mir nicht erklären kann ...

Code: Alles auswählen

    video = subprocess.Popen(VIDEO_START_COMMAND)
        ^
SyntaxError: invalid syntax
__deets__
User
Beiträge: 14493
Registriert: Mittwoch 14. Oktober 2015, 14:29

Da fehlt ne Klammer zu in der Zeile davor.
Homer-S
User
Beiträge: 30
Registriert: Freitag 1. Dezember 2017, 22:30

Hallo,

heute gig es im Büro etwas länger, deshalb komme ich erst jetzt zum Testen. Einige Error Messages konnte ich selber finden/Lösen, aber bei diesen beiden finde ich leider nichts. Darf ich euch nochmal um Hilfe bitten?

Code: Alles auswählen

[tcp @ 0x12f2700] Connection to tcp://192.168.100.111:9000 failed: Connection refused
http://192.168.100.111:9000/?action=stream: Connection refused
Aufzeichnung beendet
Traceback (most recent call last):
  File "ring.py", line 89, in <module>
    main()
  File "ring.py", line 84, in main
    record(ring_count)
  File "ring.py", line 71, in record
    send_message(CHAT_IDS, message=message)
  File "ring.py", line 40, in send_message
    params = {"chat_id": id}
UnboundLocalError: local variable 'id' referenced before assignment
Homer-S
User
Beiträge: 30
Registriert: Freitag 1. Dezember 2017, 22:30

Hallo ihr wissenden :)

ich kämpfe (tapfer) um das zum Laufen zu bekommen.

Leider komme ich mit dieser Fehlermeldung jetzt so gar nicht weiter. Dahinter habe ich noch den aktuellen code angehängt.

Ich hoffe Ihr habt noch einmal die Lust mir zu helfen.

Danke

Code: Alles auswählen

[tcp @ 0x252d700] Connection to tcp://192.168.100.111:9000 failed: Connection refused
http://192.168.100.111:9000/?action=stream: Connection refused
Aufzeichnung beendet
{'ok': False, 'description': 'Unauthorized', 'error_code': 401}
{'ok': False, 'description': 'Unauthorized', 'error_code': 401}
{'ok': False, 'description': 'Unauthorized', 'error_code': 401}
{'ok': False, 'description': 'Unauthorized', 'error_code': 401}
{'ok': False, 'description': 'Unauthorized', 'error_code': 401}
{'ok': False, 'description': 'Unauthorized', 'error_code': 401}
{'ok': False, 'description': 'Unauthorized', 'error_code': 401}
{'ok': False, 'description': 'Unauthorized', 'error_code': 401}
{'ok': False, 'description': 'Unauthorized', 'error_code': 401}
{'ok': False, 'description': 'Unauthorized', 'error_code': 401}
{'ok': False, 'description': 'Unauthorized', 'error_code': 401}
{'ok': False, 'description': 'Unauthorized', 'error_code': 401}
Videoaufzeichnung gestartet
Also nach den Fehlern, startet das Skript neu ...
Warum versucht er mit TCP zu verbinden?

Aktueller Code:

Code: Alles auswählen

#!/usr/bin/env python
#coding: utf8

import time
import os
import subprocess
import datetime
from itertools import count
import requests
import RPi.GPIO as gpio
import glob

DEVELOPERMODE = True

# PIN festlegen an dem das Signal kommt 
OPTOKOPPLER = 18
RELAIS = 11

#Telegram konfiguration
TELEGRAM_URL ="https://api.telegram.org/bot"
BOT_TOKEN = "75377000000000000000000000000000pp2w5lNY18"
KATHLEEN_ID = "7600000007"
HEINZ_ID = "70000000004"
CHAT_IDS = [HEINZ_ID, KATHLEEN_ID]

BASE_PATH = '/home/pi/cam'

VIDEO_START_COMMAND = ['mjpg_streamer',
    '-i', "/usr/local/lib/input_uvc.so -d /dev/video0 -n -r 1024x768 -f 24 -q 80"
    '-o', "/usr/local/lib/output_http.so -n -w /usr/local/www -p 9000",
    '-o', "/usr/local/lib/output_file.so -f /home/pi/cam -d 12000"]
VIDEO_CONVERT_COMMAND = ['ffmpeg',
    '-i', "http://192.168.100.111:9000/?action=stream",
    '-vcodec', 'copy', '-an', '-t', '30', '-bufsize', '2048k',
    '-y']

def send_message(chatroomids, message=None, photo=None):
    """ versendet die Klingelbenachrichtigung """
    files = {}
    global  id
    params = {"chat_id": id}
    if message:
        params["text"] = message
        cmd = "sendMessage"
    if photo:
        files = {"photo": photo}
        cmd = "sendPhoto"
    url = TELEGRAM_URL + BOT_TOKEN + "{}{}/{}".format(TELEGRAM_URL, BOT_TOKEN, cmd)
    for id in chatroomids:
        r = requests.get(url, params=params, files=files)
        result = r.json()
        print(result)

def setup():
    # Pin optokopller als Eingang, relais als Ausgang festlegen
    gpio.setmode(gpio.BOARD)
    gpio.setup(OPTOKOPPLER, gpio.IN, pull_up_down = gpio.PUD_UP)
    gpio.setup(RELAIS, gpio.OUT)
    # Klingelrelais generell anschalten
    gpio.output(RELAIS, gpio.HIGH) 

def record(ring_count):
    print("Videoaufzeichnung gestartet")
    filename = os.path.join(BASE_PATH, "Klingel-{:%Y%m%d_%H%M%S}.mp4".format(datetime.datetime.now()))
    video = subprocess.Popen(VIDEO_START_COMMAND)
    print("Video start")
    convert = subprocess.Popen(VIDEO_CONVERT_COMMAND + [filename])
    print("Aufnahme start")
    time.sleep(31)
#    video.kill()
    video.terminate()
    convert.wait()
    print("Aufzeichnung beendet")
    message = "Es hat gerade das {}. mal geklingelt.".format(ring_count)
    send_message(CHAT_IDS, message=message)
    for filename in sorted(glob.glob(os.path.join(BASE_PATH, '*.jpg'))):
        with open(filename, 'rb') as data:
            photo = data.read()
        send_message(CHAT_IDS, photo=photo)
        time.sleep(1)
        os.unlink(filename)

def main():
    try:
        setup()
        for ring_count in count():
            if DEVELOPERMODE or gpio.wait_for_edge((OPTOKOPPLER), gpio.FALLING):
                record(ring_count)
    finally:
        gpio.cleanup()

if __name__ == '__main__':
    main()
Benutzeravatar
__blackjack__
User
Beiträge: 13003
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Homer-S: HTTP läuft über TCP.

Ich sehe da ein ``global`` im Code – bitte sofort vergessen das es dieses Schlüsselwort in Python gibt. Das ist falsch. Es funktioniert ja auch gar nicht, denn der Name ist ja auch ``global`` an der Stelle gar nicht definiert. Da wird dann halt aus dem `UnboundLocalError` ein `NameError`.

Nach dem `video.terminate()` fehlt noch ein `video.wait()`. Wenn man den Prozess nicht mehr brutal vom Kernel abschlachten lässt, muss man warten bis er tatsächlich von selbst zum Ende gekommen ist.

Das die Verbindung nicht angenommen wird, kann unter Umständen an dem Problem liegen was ich schon mal angesprochen hatte: Der zweite Prozess kann sich erst zum ersten Verbinden wenn der auch auf dem Port auf Verbindungen wartet. Man müsste warten bis der tatsächlich da ist.
“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
Homer-S
User
Beiträge: 30
Registriert: Freitag 1. Dezember 2017, 22:30

__blackjack__ hat geschrieben: Freitag 10. Mai 2019, 23:14 @Homer-S: HTTP läuft über TCP.

Ich sehe da ein ``global`` im Code – bitte sofort vergessen das es dieses Schlüsselwort in Python gibt. Das ist falsch. Es funktioniert ja auch gar nicht, denn der Name ist ja auch ``global`` an der Stelle gar nicht definiert. Da wird dann halt aus dem `UnboundLocalError` ein `NameError`.

Nach dem `video.terminate()` fehlt noch ein `video.wait()`. Wenn man den Prozess nicht mehr brutal vom Kernel abschlachten lässt, muss man warten bis er tatsächlich von selbst zum Ende gekommen ist.

Das die Verbindung nicht angenommen wird, kann unter Umständen an dem Problem liegen was ich schon mal angesprochen hatte: Der zweite Prozess kann sich erst zum ersten Verbinden wenn der auch auf dem Port auf Verbindungen wartet. Man müsste warten bis der tatsächlich da ist.
Wenn ich das "global id" wieder raus nehme, bekomme ich diesen Fehler:

UnboundLocalError: local variable 'id' referenced before assignment

Dazu hatte ich nur das mit dem global gefunden ...???


Wegen dem "zweiten Prozess" ... wieso hatte das in meiner gestümperten Version dann aber funktioniert?
Kann es da ein Zugriffs- Rechte-Problem geben? Wenn ja was müsste ich prüfen?
Antworten