Vogelalbum 1.0.0
Eine Kamera am Vogelhaus, ein Rechner der erkennt wer da war — und eine Sammlung, die sich von allein fuellt. Zwei neuronale Netze: eines findet den Vogel im Bild, eines bestimmt die Art. 148 heimische Arten, auf Deutschland eingeschraenkt und neu normiert — ohne diese Einschraenkung wird aus einer Kohlmeise eine nordamerikanische Schwesterart. Daraus entsteht ein Sammelalbum: nummerierte Seiten, gefundene Arten in Farbe, fehlende als Schatten, dazu Erstfang, Punkte, Stufen und 15 Abzeichen. Laeuft vollstaendig auf eigener Hardware. Kein Cloud-Dienst, kein Konto, keine Telemetrie; die Bilder verlassen das eigene Netz nicht. Zwei getrennt deploybare Teile: die Sammlung als Django-Anwendung, die Erkennung als eigener Dienst. Der Erkennungs-Dienst fragt selbst an und braucht deshalb weder eine erreichbare Adresse noch einen offenen Port — er laeuft hinter einem gewoehnlichen Router. Dabei: Firmware fuer Raspberry Pi und ESP32-S3, Anbindung an Home Assistant ueber MQTT, Oberflaeche auf Deutsch und Englisch, Einstellungen je Kamera aus dem Browser, und ein Weg fuer Fotos von unterwegs. AGPL-3.0.
This commit is contained in:
commit
c11fe9ab04
189 files changed
+29806
No files matched your search
@@ -0,0 +1,129 @@
|
||||
"""Meldungen über ntfy — Sichtungen und Störungen.
|
||||
|
||||
Was hier bewusst NICHT passiert: die Anwendung aufhalten. Eine Meldung ist
|
||||
eine Nebensache; wenn der ntfy-Server nicht antwortet, darf das weder die
|
||||
Antwort an den Detector verzögern noch einen Fehler auslösen. Eine verpasste
|
||||
Meldung ist ärgerlich, eine verlorene Sichtung wäre schlimmer.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import threading
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
|
||||
from .models import Benachrichtigung
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
ZEITGRENZE = 8 # Sekunden
|
||||
|
||||
|
||||
def _senden(ziel: str, titel: str, text: str, prioritaet: str, symbole: str):
|
||||
"""Schickt die Meldung. Läuft im eigenen Faden, siehe _im_hintergrund."""
|
||||
try:
|
||||
anfrage = urllib.request.Request(
|
||||
ziel,
|
||||
data=text.encode("utf-8"),
|
||||
headers={
|
||||
"Title": titel.encode("utf-8"),
|
||||
"Priority": prioritaet,
|
||||
"Tags": symbole,
|
||||
},
|
||||
method="POST",
|
||||
)
|
||||
with urllib.request.urlopen(anfrage, timeout=ZEITGRENZE):
|
||||
pass
|
||||
except (urllib.error.URLError, OSError, ValueError) as fehler:
|
||||
# Nur protokollieren. Wer eine Meldung nicht zustellen kann, soll
|
||||
# deshalb nicht den Betrieb anhalten.
|
||||
log.warning("ntfy nicht erreicht: %s", fehler)
|
||||
|
||||
|
||||
def _im_hintergrund(*args):
|
||||
threading.Thread(target=_senden, args=args, daemon=True).start()
|
||||
|
||||
|
||||
def melde_sichtung(sighting, ist_neue_art: bool) -> None:
|
||||
"""Meldet eine Sichtung, je nach Einstellung im Backend."""
|
||||
e = Benachrichtigung.laden()
|
||||
if not e.aktiv or not e.ziel:
|
||||
return
|
||||
|
||||
if e.sichtungen == Benachrichtigung.Sichtungen.AUS:
|
||||
return
|
||||
if e.sichtungen == Benachrichtigung.Sichtungen.NEUE and not ist_neue_art:
|
||||
return
|
||||
|
||||
art = sighting.species.german if sighting.species else "Unbestimmt"
|
||||
kamera = sighting.camera.name if sighting.camera else "unbekannte Kamera"
|
||||
sicherheit = round(sighting.confidence * 100)
|
||||
|
||||
if ist_neue_art:
|
||||
titel = f"Neue Art: {art}"
|
||||
text = f"{kamera} · {sicherheit} % · zum ersten Mal gesehen"
|
||||
symbole = "sparkles"
|
||||
prioritaet = "default"
|
||||
else:
|
||||
titel = art
|
||||
text = f"{kamera} · {sicherheit} %"
|
||||
symbole = "bird"
|
||||
# Leise: Wer alle Sichtungen meldet, will nicht bei jedem Spatz
|
||||
# ein Summen am Handy.
|
||||
prioritaet = "low"
|
||||
|
||||
_im_hintergrund(e.ziel, titel, text, prioritaet, symbole)
|
||||
|
||||
|
||||
def melde_stoerung(titel: str, text: str) -> None:
|
||||
"""Meldet eine Störung — laut, weil sie Handeln erfordert.
|
||||
|
||||
🔑 **Synchron, im Gegensatz zu den Sichtungen.** Und das ist keine
|
||||
Unachtsamkeit, sondern der Grund, warum diese Meldung am 24.09.2026
|
||||
nie ankam:
|
||||
|
||||
Störungen meldet ausschließlich der Befehl ``stoerungen_pruefen``, und
|
||||
der ist ein Einmal-Lauf. Ein Hintergrundfaden ist dort ein
|
||||
Daemon-Faden — der stirbt, sobald der Prozess endet. Der Befehl war
|
||||
mit seiner Arbeit fertig, bevor die Meldung draußen war, und hat sie
|
||||
damit mitgenommen.
|
||||
|
||||
⚠️ Bei ``melde_sichtung`` ist der Hintergrundfaden richtig: Die läuft
|
||||
in einer Web-Anfrage, die weiterlebt, und darf den Detector nicht
|
||||
warten lassen. Hier gibt es nichts, was wartet -- das Senden IST die
|
||||
Arbeit.
|
||||
"""
|
||||
e = Benachrichtigung.laden()
|
||||
if not e.aktiv or not e.stoerungen or not e.ziel:
|
||||
return
|
||||
_senden(e.ziel, titel, text, "high", "rotating_light")
|
||||
|
||||
|
||||
def melde_probe() -> tuple[bool, str]:
|
||||
"""Sofort und synchron — für den Knopf im Backend.
|
||||
|
||||
Hier wird ausnahmsweise gewartet: Wer auf 'Probe senden' drückt, will
|
||||
wissen, ob es geklappt hat. Eine Meldung, die still im Hintergrund
|
||||
scheitert, wäre genau das Gegenteil einer Probe.
|
||||
"""
|
||||
e = Benachrichtigung.laden()
|
||||
if not e.ziel:
|
||||
return False, "Kein Thema eingetragen."
|
||||
try:
|
||||
anfrage = urllib.request.Request(
|
||||
e.ziel,
|
||||
data="Wenn du das liest, funktioniert es.".encode("utf-8"),
|
||||
headers={
|
||||
"Title": "Vogelalbum Probe".encode("utf-8"),
|
||||
"Priority": "default",
|
||||
"Tags": "white_check_mark",
|
||||
},
|
||||
method="POST",
|
||||
)
|
||||
with urllib.request.urlopen(anfrage, timeout=ZEITGRENZE) as antwort:
|
||||
return True, f"Gesendet (HTTP {antwort.status})."
|
||||
except urllib.error.HTTPError as fehler:
|
||||
return False, f"Server antwortet mit {fehler.code}."
|
||||
except (urllib.error.URLError, OSError, ValueError) as fehler:
|
||||
return False, f"Nicht erreicht: {fehler}"
|
||||
Reference in new issue
Block a user