Files
admin 0a893d0684 Helligkeit: zwei Werte und eine Schwelle, zur Laufzeit aenderbar
Die vier abgestuften Helligkeiten des alten MSP430-Programms (5/15/50/100 %)
entfallen. Fuer die Sternwarte genuegt der Wechsel Tag/Nacht: unterhalb der
Schwelle laeuft die Anzeige mit dunkel_prozent, oberhalb mit hell_prozent.

Die Werte stehen nicht mehr im Code, sondern in settings.json auf dem Geraet --
Vorbereitung fuer die spaetere MQTT-Anbindung:

    settings.load()                        # mit Vorgaben aufgefuellt
    settings.update({"hell_prozent": 60})  # aendern und speichern

Vorgaben: dunkel 5 %, hell 50 %, Schwelle 1800, Hysterese 150. Die Schwelle liegt
mit Abstand zwischen den gemessenen Lagen (abgedeckt 0, Raumlicht 2453).

Beibehalten, weil die Messung vom 27.07. sie verlangt: der gleitende Mittelwert
und eine Hysterese um die Schwelle. Der ADC rauscht mit +/-32 (nach Mittelung,
1 Sigma) -- ohne Hysterese wuerde die Anzeige am Umschaltpunkt springen.

settings.py prueft jeden Wert beim Laden UND beim Schreiben auf Typ und Bereich.
Das ist der eigentliche Zweck: Mit MQTT kommen die Werte von aussen, und ein
Helligkeitswert von 500 % oder eine Schwelle von -1 wuerde die Anzeige sonst
unbrauchbar machen. Eine fehlende oder beschaedigte Datei faellt auf die Vorgaben
zurueck, ein einzelner kaputter Wert verwirft nur diesen -- ein Geraet in der
Sternwarte soll sich nicht an seiner Konfigurationsdatei aufhaengen.

settings.json wird von deploy.sh bewusst NICHT mitkopiert und steht in
.gitignore: Es ist Laufzeitzustand, kein Quellcode. Sonst waeren die
eingestellten Werte nach jedem Deployen weg.

Am Geraet bestaetigt: schreiben, wiederlesen, Controller uebernimmt die Werte,
beide Helligkeiten auf der Anzeige sichtbar. 119 Tests gruen.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 16:34:47 +02:00

168 lines
6.1 KiB
Python

#!/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())