"""Einstellungen, die sich im Betrieb aendern lassen -- als JSON auf dem Geraet. config.py ist Programmcode: Pinbelegung, Protokoll, Geometrie. Das aendert sich nur beim Deployen. Werte, die im laufenden Betrieb verstellt werden sollen -- spaeter ueber MQTT -- gehoeren dagegen hierher. Sie liegen als JSON im Dateisystem (settings.json) und lassen sich lesen und zurueckschreiben, ohne das Programm neu zu spielen. import settings w = settings.load() # mit Vorgaben aufgefuellt settings.update({"hell_prozent": 60}) # aendern und speichern Fehlt die Datei oder ist sie beschaedigt, gelten die Vorgaben aus DEFAULTS -- die Anzeige laeuft dann weiter, statt beim Start auszufallen. Das ist Absicht: ein Geraet in der Sternwarte soll sich nicht an einer kaputten Konfigurationsdatei aufhaengen. Alle Werte werden beim Laden UND beim Schreiben geprueft (siehe _pruefe). Das ist wichtig, sobald MQTT dazukommt: Von dort kommen fremde Eingaben, und ein Helligkeitswert von 500 % oder eine Schwelle von -1 wuerde die Anzeige sonst in einen unbrauchbaren Zustand bringen. Es gibt zwei Sorten von Werten: ganze Zahlen mit einem erlaubten Bereich (_GRENZEN) und Zeichenketten mit erlaubtem Zeichenvorrat (mount_host). Welche Sorte gilt, steht am Typ der Vorgabe in DEFAULTS. """ import json PFAD = "settings.json" # Vorgaben. Die Helligkeiten und die Schwelle stammen aus der LDR-Messung vom # 2026-07-27 (siehe README, Abschnitt Helligkeitsregelung): abgedeckt ergab 0, # Raumlicht 2453, angeleuchtet 3552 -- 1800 liegt mit Abstand zwischen dunkel # und Raumlicht. Die Hysterese deckt das gemessene ADC-Rauschen ab (~32 nach # der Mittelung, 1 Sigma). DEFAULTS = { "dunkel_prozent": 5, # Helligkeit unterhalb der Schwelle "hell_prozent": 50, # Helligkeit oberhalb der Schwelle "schwelle": 1800, # LDR-Wert (nach raw_to_ldr) fuer den Umschaltpunkt "hysterese": 150, # Breite um die Schwelle, gegen Flattern # Adresse der Montierung (IP oder Hostname). **Leer heisst: config.MOUNT_HOST # gilt.** Damit bleibt die im Code eingetragene Adresse die Vorgabe -- eine # einmal per MQTT gesetzte Adresse wuerde sie sonst fuer immer verdecken, # auch nach einem Deployen mit geaendertem config.py. Zuruecksetzen also mit # einer leeren Nachricht an /set/mount_host. "mount_host": "", } # Erlaubte Bereiche der Zahlenwerte: (minimum, maximum). Die Prozente gehen an # ESC H, das nur 0..100 kennt; die LDR-Werte kommen aus einem 12-Bit-ADC. _GRENZEN = { "dunkel_prozent": (0, 100), "hell_prozent": (0, 100), "schwelle": (0, 4095), "hysterese": (0, 2000), } # Zeichenketten: nur das, was in einer IP-Adresse oder einem Hostnamen vorkommen # darf. Bewusst eng -- der Wert geht ungeprueft in socket.getaddrinfo, und von # MQTT kommt alles Moegliche. 63 Zeichen ist die Laenge eines DNS-Labels und # reicht fuer jede IP und jeden Namen im Sternwartennetz. HOST_ZEICHEN = ("abcdefghijklmnopqrstuvwxyz" "ABCDEFGHIJKLMNOPQRSTUVWXYZ" "0123456789.-") HOST_MAXLEN = 63 class SettingsError(ValueError): """Ein Wert liegt ausserhalb des erlaubten Bereichs oder ist unbekannt.""" def _pruefe_zahl(name, wert): # bool ist in Python ein int -- hier aber sicher ein Irrtum. if isinstance(wert, bool) or not isinstance(wert, int): raise SettingsError("%s: %r ist keine ganze Zahl" % (name, wert)) low, high = _GRENZEN[name] if not low <= wert <= high: raise SettingsError("%s: %d liegt ausserhalb %d..%d" % (name, wert, low, high)) return wert def _pruefe_text(name, wert): if not isinstance(wert, str): raise SettingsError("%s: %r ist keine Zeichenkette" % (name, wert)) wert = wert.strip() if len(wert) > HOST_MAXLEN: raise SettingsError("%s: laenger als %d Zeichen" % (name, HOST_MAXLEN)) for zeichen in wert: if zeichen not in HOST_ZEICHEN: raise SettingsError("%s: %r enthaelt das unerlaubte Zeichen %r" % (name, wert, zeichen)) return wert def _pruefe(werte): """Werte auf Typ und Bereich pruefen. Liefert ein geprueftes neues dict. Ob eine Einstellung eine Zahl oder eine Zeichenkette ist, entscheidet der Typ ihrer Vorgabe in DEFAULTS -- so gibt es nur eine Stelle, an der eine neue Einstellung eingetragen werden muss. """ geprueft = {} for name, wert in werte.items(): if name not in DEFAULTS: raise SettingsError("unbekannte Einstellung %r" % (name,)) if isinstance(DEFAULTS[name], str): geprueft[name] = _pruefe_text(name, wert) else: geprueft[name] = _pruefe_zahl(name, wert) return geprueft def load(pfad=None, log=None): """Einstellungen lesen, fehlende Werte aus DEFAULTS ergaenzen. Wirft nicht: Fehlt die Datei, ist sie unlesbar oder steht Unsinn darin, gibt es die Vorgaben. Ein einzelner unbrauchbarer Wert verwirft nur diesen, nicht die ganze Datei. """ pfad = PFAD if pfad is None else pfad werte = dict(DEFAULTS) try: with open(pfad) as f: gelesen = json.load(f) except (OSError, ValueError): return werte # keine Datei oder kaputtes JSON if not isinstance(gelesen, dict): return werte for name, wert in gelesen.items(): try: werte.update(_pruefe({name: wert})) except SettingsError as e: if log: log("settings: %s -- Vorgabe %r bleibt" % (e, DEFAULTS.get(name))) return werte def save(werte, pfad=None): """Gepruefte Einstellungen schreiben. Liefert die gespeicherten Werte.""" pfad = PFAD if pfad is None else pfad vollstaendig = dict(DEFAULTS) vollstaendig.update(_pruefe(werte)) with open(pfad, "w") as f: json.dump(vollstaendig, f) return vollstaendig def update(aenderungen, pfad=None, log=None): """Einzelne Werte aendern und speichern -- der Weg fuer MQTT. Erst pruefen, dann mit dem Bestand mischen, dann schreiben. Ist etwas ungueltig, bleibt die Datei unveraendert. """ geprueft = _pruefe(aenderungen) werte = load(pfad, log=log) werte.update(geprueft) return save(werte, pfad)