#!/usr/bin/env python3 """Helligkeitsumschaltung anhand eines LDR-Werts. Zwei Helligkeiten, ein Umschaltpunkt: unterhalb der Schwelle laeuft die Anzeige mit "dunkel", oberhalb mit "hell". Die vier abgestuften Helligkeiten des alten MSP430-Programms (DoDimmen_V, 5/15/50/100 %) sind bewusst aufgegeben -- fuer die Sternwarte genuegt der Wechsel Tag/Nacht. Die Werte stehen nicht hier, sondern in settings.json (siehe settings.py), damit sie sich im Betrieb aendern lassen -- spaeter ueber MQTT. Zwei Dinge aus der alten Regelung bleiben, weil die Messung am Geraet sie verlangt (2026-07-27): - **gleitender Mittelwert** ueber AVERAGE_WINDOW Werte: der ADC des ESP32-C3 rauscht kraeftig (Rohwert +/-270 bei Raumlicht). - **Hysterese** um die Schwelle: nach der Mittelung bleiben rund +/-32 (1 Sigma). Ohne Hysterese wuerde die Anzeige am Umschaltpunkt zwischen den beiden Helligkeiten hin- und herspringen. Der Fotowiderstand liefert einen Rohwert (12-bit-ADC, 0..4095). Gerechnet wird in der Richtung "grosser Wert = helle Umgebung"; auf der ESP32-Platine ist die Kennlinie umgekehrt, deshalb laeuft der Rohwert zuerst durch raw_to_ldr(). Reines Python, laeuft auch unter MicroPython. python3 brightness.py --ldr 1500 # Helligkeit berechnen und senden python3 brightness.py --ldr 200 --dry-run """ # argparse und sys.stderr gibt es unter MicroPython nicht -- beides wird nur vom # CLI-Teil ganz unten gebraucht und darum erst dort importiert. So laesst sich # dieses Modul auf dem ESP32 einfach importieren. AVERAGE_WINDOW = 8 # MAXHELLARRAY: gleitender Mittelwert ueber 8 Werte # Die beiden Helligkeitsstufen. DUNKEL gilt unterhalb der Schwelle, HELL darueber. DUNKEL = 0 HELL = 1 def raw_to_ldr(raw, inverted=None, full_scale=None): """ADC-Rohwert in die Zaehlrichtung bringen, die die Schwellen erwarten. Die Schwellen oben stammen vom alten MSP430-Aufbau: hoher Wert = helle Umgebung. Auf der ESP32-Platine liegt der LDR gegen Masse und der 2k gegen 3V3, dort faellt die Spannung an A0 mit steigender Helligkeit. Der Wert wird deshalb gespiegelt, wenn config.LDR_INVERTED gesetzt ist. """ import config if inverted is None: inverted = config.LDR_INVERTED if full_scale is None: full_scale = config.LDR_MAX return (full_scale - raw) if inverted else raw class BrightnessController: """Schaltet anhand des LDR-Werts zwischen zwei Helligkeiten um. update() meldet zurueck, ob sich die Stufe geaendert hat -- nur dann wird ein Helligkeitstelegramm gesendet, wie schon im alten Programm. Die Werte kommen aus settings.json und werden beim Anlegen einmal gelesen. Wer sie im Betrieb aendert (spaeter ueber MQTT), ruft danach reload() auf. """ def __init__(self, werte=None, pfad=None, log=None): import settings self._settings = settings self._pfad = pfad self._werte = settings.load(pfad, log=log) if werte is None else dict(werte) self._buf = [] self._level = None @property def level(self): return self._level @property def settings(self): """Die aktuell verwendeten Werte (Kopie).""" return dict(self._werte) def reload(self, log=None): """Einstellungen neu aus der Datei lesen. Die Stufe bleibt stehen; ob sie zu den neuen Werten noch passt, klaert der naechste update() -- inklusive Hysterese, also ohne Sprung. """ self._werte = self._settings.load(self._pfad, log=log) return self.settings def percent(self, level): """Prozentwert zu einer Stufe.""" return self._werte["hell_prozent" if level == HELL else "dunkel_prozent"] def _next_level(self, val, old): """Stufe mit Hysterese bestimmen. Umgeschaltet wird erst, wenn der Wert die Schwelle um die Hysterese ueberschreitet -- nach oben auf HELL, nach unten auf DUNKEL. Dazwischen bleibt es bei der bisherigen Stufe; genau das verhindert das Flattern. Beim ersten Aufruf (old=None) gibt es keine bisherige Stufe. Dann entscheidet die blanke Schwelle, damit die Anzeige sofort richtig startet, statt erst nach der halben Hysterese aufzuwachen. """ schwelle = self._werte["schwelle"] hyst = self._werte["hysterese"] if old is None: return HELL if val >= schwelle else DUNKEL if val >= schwelle + hyst: return HELL if val < schwelle - hyst: return DUNKEL return old def update(self, ldr): """Einen LDR-Wert einspeisen. Liefert (stufe, prozent, geaendert).""" self._buf.append(ldr) if len(self._buf) > AVERAGE_WINDOW: self._buf.pop(0) avg = sum(self._buf) // len(self._buf) level = self._next_level(avg, self._level) changed = level != self._level self._level = level return level, self.percent(level), changed def main(argv=None): import argparse import sys p = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) p.add_argument("--ldr", type=int, required=True, help="LDR-Rohwert (0..4095)") p.add_argument("--port", default=None, help="serieller Port (Vorgabe aus config.py)") p.add_argument("--dry-run", action="store_true", help="nur berechnen/anzeigen, nicht senden") args = p.parse_args(argv) c = BrightnessController() level, percent, _ = c.update(args.ldr) w = c.settings print("Schwelle %d (+/-%d) -> dunkel %d %%, hell %d %%" % (w["schwelle"], w["hysterese"], w["dunkel_prozent"], w["hell_prozent"])) print("LDR %d -> %s -> %d %%" % (args.ldr, "HELL" if level == HELL else "DUNKEL", percent)) import config import migra from display import open_display d = open_display(port=args.port or config.PORT, fake=args.dry_run) try: d.set_brightness(percent) except migra.MigraError as e: print("Fehler: %s" % e, file=sys.stderr) return 1 finally: d.close() print("OK") return 0 if __name__ == "__main__": import sys sys.exit(main())