"""Prüft, ob die Kameras noch melden — und schlägt Alarm, wenn nicht. 🔑 Warum das hier liegt und nicht im Detector: Ein abgestürzter Prozess kann seinen eigenen Tod nicht melden. Wer das Ausbleiben von Meldungen bemerken soll, muss von aussen schauen. Diese Seite hier läuft ohnehin. Gedacht als regelmässiger Aufruf (systemd-Zeitgeber oder cron): */10 * * * * .../manage.py stoerungen_pruefen ⚠ Es wird pro Kamera nur EINMAL gemeldet, nicht bei jedem Durchlauf. Eine Störung, die alle zehn Minuten aufs Handy summt, schaltet man nach einer Stunde ab — und dann merkt man die nächste nicht mehr. 🔑 **Der Merker steht in der Datenbank, nicht im Prozess.** Hier stand einmal ein Modul-Set `_gemeldet`. In einem Einmal-Lauf ist das wirkungslos: jeder Aufruf ist ein frischer Prozess, das Set beim Start wieder leer. Die Zusage eine Zeile weiter oben war damit acht Monate lang unwahr, und am 01.10.2026 hat es sich gezeigt -- nach dem Stromausfall summten beide Kameras im Zehnminutentakt. """ from django.core.management.base import BaseCommand from django.utils import timezone from dex import benachrichtigung as bn from dex.models import Benachrichtigung, Camera, Sighting class Command(BaseCommand): help = "Meldet Kameras, die sich länger nicht gemeldet haben" def add_arguments(self, parser): parser.add_argument( "--trocken", action="store_true", help="Nur anzeigen, nichts senden", ) def handle(self, *args, **optionen): einstellung = Benachrichtigung.laden() grenze = timezone.now() - timezone.timedelta( minutes=einstellung.stille_minuten) # ⚠️ Mobile Geräte bleiben außen vor. Die Prüfung misst, ob sich ein # Gerät länger als erlaubt nicht gemeldet hat — das ergibt nur bei # Dauerbetrieb einen Sinn. Ein Handy meldet sich, wenn jemand # spazieren geht, und nach dem ersten ruhigen Wochenende stünde # sonst „Kamera still: Handy" auf dem Telefon. Genau so entstehen # Fehlalarme, die man abschaltet — und dann bemerkt man den echten # Ausfall nicht mehr. for kamera in Camera.objects.filter(active=True, mobil=False): # ⚠ Gemessen wird das LEBENSZEICHEN, nicht die letzte Sichtung. # An last_seen zu messen waere der naheliegende Fehler: An # einem ruhigen Nachmittag kommt stundenlang kein Vogel, und # es gaebe jeden Tag Fehlalarm. (Genau so hatte ich es zuerst # gebaut -- der Trockenlauf meldete prompt "still seit 824 # min" fuer eine tadellos laufende Kamera.) if kamera.letzter_kontakt is None: # Noch nie gemeldet. Das ist keine Stoerung, sondern eine # Kamera, die gerade erst eingerichtet wurde. self.stdout.write(f"{kamera.name}: noch kein Lebenszeichen") continue still_seit = timezone.now() - kamera.letzter_kontakt minuten = int(still_seit.total_seconds() // 60) if kamera.letzter_kontakt < grenze: if kamera.stoerung_gemeldet_seit is not None: self.stdout.write(f"{kamera.name}: still ({minuten} min), " "schon gemeldet") continue self.stdout.write(self.style.ERROR( f"{kamera.name}: STILL seit {minuten} min")) if not optionen["trocken"]: bn.melde_stoerung( f"Kamera still: {kamera.name}", f"Seit {minuten} Minuten kein Lebenszeichen. " f"Läuft die Erkennung noch?") # ⚠ Erst nach dem Senden vermerken. melde_stoerung # sendet synchron und schluckt Fehler nur im Log -- # wer vorher vermerkt, verliert bei einem Aussetzer # des Meldewegs die Stoerung still und fuer immer. kamera.stoerung_gemeldet_seit = timezone.now() kamera.save(update_fields=["stoerung_gemeldet_seit"]) else: if kamera.stoerung_gemeldet_seit is not None: # Wieder da — das gehört auch gemeldet, sonst weiss # niemand, ob das Problem noch besteht. self.stdout.write(self.style.SUCCESS( f"{kamera.name}: wieder da")) if not optionen["trocken"]: bn.melde_stoerung( f"Kamera wieder da: {kamera.name}", "Sie meldet sich wieder.") kamera.stoerung_gemeldet_seit = None kamera.save(update_fields=["stoerung_gemeldet_seit"]) else: self.stdout.write(f"{kamera.name}: ok ({minuten} min her)") self._wartende_fotos(einstellung, optionen["trocken"]) def _wartende_fotos(self, einstellung, trocken: bool) -> None: """Meldet, wenn hochgeladene Fotos zu lange auf den Detector warten. 🔑 **Das ist die Stillstandsprüfung für den Detector selbst.** Die Kameraprüfung oben merkt, wenn ein Gerät nichts mehr liefert — sie merkt aber nicht, wenn der Detector steht, denn der meldet sich bei niemandem an. Liegengebliebene Fotos sind das einzige sichtbare Zeichen dafür, und sie sagen es unmissverständlich: Jemand hat hochgeladen, und seitdem ist nichts passiert. 📌 Verworfen wird nichts. Das Foto vom Spaziergang gibt es kein zweites Mal, und stilles Löschen wäre der schlechteste Umgang damit. """ wartend = Sighting.objects.wartend() aeltestes = wartend.first() if aeltestes is None: if einstellung.fotos_gemeldet_seit is not None: self.stdout.write(self.style.SUCCESS("Fotos: alle bearbeitet")) if not trocken: bn.melde_stoerung( "Fotos wieder bearbeitet", "Die liegengebliebenen Fotos sind bestimmt.") einstellung.fotos_gemeldet_seit = None einstellung.save(update_fields=["fotos_gemeldet_seit"]) return minuten = int( (timezone.now() - aeltestes.started_at).total_seconds() // 60) anzahl = wartend.count() if minuten < einstellung.stille_minuten: self.stdout.write( f"{anzahl} Foto(s) warten, ältestes {minuten} min — ok") return if einstellung.fotos_gemeldet_seit is not None: self.stdout.write( f"{anzahl} Foto(s) warten ({minuten} min), schon gemeldet") return self.stdout.write(self.style.ERROR( f"{anzahl} Foto(s) warten auf Bestimmung, " f"ältestes seit {minuten} min")) if not trocken: bn.melde_stoerung( "Fotos warten auf Bestimmung", f"{anzahl} hochgeladene Foto(s) liegen seit {minuten} Minuten " f"unbearbeitet. Läuft der Detector noch?") # Erst nach dem Senden vermerken — siehe oben bei den Kameras. einstellung.fotos_gemeldet_seit = timezone.now() einstellung.save(update_fields=["fotos_gemeldet_seit"])