"""Abschaltung bei Dunkelheit. Eine Kamera ohne Infrarot liefert nachts ein schwarzes Bild. Es durch zwei neuronale Netze zu schicken kostet Strom und bringt nichts. Entschieden wird über die **tatsächliche Bildhelligkeit**, nicht über Sonnenauf- und -untergang. Zwei Gründe: Es braucht keine Koordinaten und keine Zeitzonenrechnerei, und es misst genau das, worauf es ankommt — ob man etwas sieht. Ein Futterhaus unter einer Straßenlaterne bleibt nutzbar, ein zugewachsenes ist schon vor Sonnenuntergang zu dunkel. Der Kalender weiß das nicht, das Bild schon. Angenehmer Nebeneffekt: Eine Kamera mit Infrarot liefert nachts ein graues, aber helles Bild. Sie läuft damit von selbst weiter, ohne dass jemand etwas einstellen muss. Der ausdrückliche Schalter (``enabled=False``) bleibt trotzdem, falls die Automatik danebenliegt. Die Voreinstellungen sind an der Kamera am Vogelhaus gemessen, nicht geraten: tagsüber 111–131, nachts 1,7 von 255. """ from __future__ import annotations import numpy as np from PIL import Image def brightness(img: Image.Image, width: int = 96) -> float: """Mittlere Helligkeit 0–255. Auf einem winzigen Graubild, das kostet nichts.""" h = max(1, round(img.height * width / img.width)) small = img.convert("L").resize((width, h), Image.BILINEAR) return float(np.asarray(small, dtype=np.float32).mean()) class DaylightGate: """Entscheidet, ob es hell genug zum Analysieren ist. Mit Hysterese: Der Wert zum Abschalten liegt deutlich unter dem zum Wiedereinschalten. Ohne das flattert die Entscheidung in der Dämmerung im Sekundentakt hin und her, und jede vorbeifahrende Autoscheinwerfer-Reflexion startet einen neuen Besuch. """ def __init__( self, dark_below: float = 15.0, light_above: float = 30.0, patience: int = 20, enabled: bool = True, ): if light_above <= dark_below: raise ValueError( "light_above muss über dark_below liegen, sonst gibt es keine " "Hysterese und die Entscheidung flattert." ) self.dark_below = dark_below self.light_above = light_above self.patience = patience self.enabled = enabled self.is_dark = False self.last_brightness = 255.0 self._streak = 0 def update(self, img: Image.Image) -> bool: """Bild einspeisen. Gibt zurück, ob analysiert werden soll.""" if not self.enabled: return True self.last_brightness = brightness(img) # Zählt nur, was für einen Wechsel spricht. Ein einzelnes helles oder # dunkles Bild setzt die Strähne zurück. will_wechseln = ( self.last_brightness >= self.light_above if self.is_dark else self.last_brightness < self.dark_below ) self._streak = self._streak + 1 if will_wechseln else 0 if self._streak >= self.patience: self.is_dark = not self.is_dark self._streak = 0 return not self.is_dark @property def status(self) -> str: if not self.enabled: return "Helligkeitsabschaltung aus (Nachtsicht)" zustand = "dunkel — pausiert" if self.is_dark else "hell genug" return f"{zustand}, Helligkeit {self.last_brightness:.0f}/255"