"""Kameras anlegen, auflisten, Token erneuern. manage.py camera add vogelhaus --name "Futterhaus Garten" manage.py camera list manage.py camera rotate vogelhaus manage.py camera disable vogelhaus Der Token wird genau einmal ausgegeben — danach liegt nur noch sein Hash in der Datenbank. Wer ihn verliert, erzeugt mit ``rotate`` einen neuen. """ from __future__ import annotations from django.core.management.base import BaseCommand, CommandError from django.utils import timezone from django.utils.text import slugify from dex.models import Camera class Command(BaseCommand): help = "Kameras verwalten." def add_arguments(self, parser): sub = parser.add_subparsers(dest="action", required=True) add = sub.add_parser("add", help="Neue Kamera anlegen") add.add_argument("slug") add.add_argument("--name", help="Anzeigename (Standard: aus dem Slug)") add.add_argument("--location", default="", help="Standort, z.B. 'Garten Nord'") # 🔑 Ein Handy ist eine Kamera, die DARF schweigen. Ohne diese Angabe # trägt es im Kamerafilter und im Störungswächter dauerhaft ein ⚠, # weil beide "seit Stunden nichts geliefert" als Defekt lesen — richtig # für ein Vogelhaus, falsch für ein Telefon in der Tasche. Bisher gab # es das Feld nur im Admin; wer eine Handy-Kamera auf der # Kommandozeile anlegt, hat es zwangsläufig falsch. add.add_argument( "--mobil", action="store_true", help="Handy o.ä.: liefert nur gelegentlich, gilt nie als gestört.", ) sub.add_parser("list", help="Alle Kameras anzeigen") rot = sub.add_parser("rotate", help="Neuen Token erzeugen") rot.add_argument("slug") dis = sub.add_parser("disable", help="Kamera stilllegen") dis.add_argument("slug") ena = sub.add_parser("enable", help="Kamera wieder aktivieren") ena.add_argument("slug") def handle(self, *args, **opts): action = opts["action"] if action == "add": return self._add(opts) if action == "list": return self._list() return self._toggle(action, opts["slug"]) def _add(self, opts): slug = slugify(opts["slug"]) if Camera.objects.filter(slug=slug).exists(): raise CommandError(f"Kamera '{slug}' gibt es schon.") token = Camera.new_token() camera = Camera( slug=slug, name=opts.get("name") or slug.replace("-", " ").title(), location=opts.get("location") or "", mobil=bool(opts.get("mobil")), ) camera.set_token(token) camera.save() self.stdout.write(self.style.SUCCESS(f"\nKamera '{camera.name}' angelegt.\n")) self.stdout.write("Token (wird nur dieses eine Mal angezeigt):\n") self.stdout.write(self.style.WARNING(f" {token}\n")) if camera.mobil: # Eine mobile Kamera hat keinen Erkennungs-Host — der Token geht # in die App. Der Hinweis unten wäre hier eine falsche Fährte. self.stdout.write("In der App unter 'Server verbinden' eintragen.\n") else: self.stdout.write( "Auf dem Erkennungs-Host in /etc/vogelalbum.env eintragen:\n" ) self.stdout.write(f" VOGELALBUM_TOKEN={token}\n") def _list(self): cameras = Camera.objects.all() if not cameras: self.stdout.write("Keine Kameras angelegt.") return self.stdout.write(f"\n{'Slug':<18} {'Name':<24} {'Aktiv':<6} {'Zuletzt':<18} Sichtungen") self.stdout.write("-" * 82) for c in cameras: last = ( timezone.localtime(c.last_seen).strftime("%d.%m. %H:%M:%S") if c.last_seen else "nie" ) flag = "ja" if c.active else "nein" warn = " ⚠ hängt?" if c.active and c.is_stale else "" self.stdout.write( f"{c.slug:<18} {c.name:<24} {flag:<6} {last:<18} " f"{c.sightings.count()}{warn}" ) def _toggle(self, action: str, slug: str): camera = Camera.objects.filter(slug=slug).first() if camera is None: raise CommandError(f"Keine Kamera '{slug}'.") if action == "rotate": token = Camera.new_token() camera.set_token(token) camera.save(update_fields=["token_hash"]) self.stdout.write(self.style.SUCCESS(f"Neuer Token für '{camera.name}':")) self.stdout.write(self.style.WARNING(f" {token}")) self.stdout.write("Der alte Token ist ab sofort ungültig.") return camera.active = action == "enable" camera.save(update_fields=["active"]) state = "aktiviert" if camera.active else "stillgelegt" self.stdout.write(self.style.SUCCESS(f"'{camera.name}' {state}."))