Compare commits
1 Commits
0876d8e827
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 4461035777 |
@@ -167,8 +167,12 @@ mpremote repl
|
|||||||
>>> run_esp32.main() # ohne Helligkeitsregelung
|
>>> run_esp32.main() # ohne Helligkeitsregelung
|
||||||
>>> run_esp32.main(with_ldr=True) # mit LDR über den ADC
|
>>> run_esp32.main(with_ldr=True) # mit LDR über den ADC
|
||||||
>>> run_esp32.main(with_mqtt=False) # ohne MQTT (sonst an, sofern konfiguriert)
|
>>> run_esp32.main(with_mqtt=False) # ohne MQTT (sonst an, sofern konfiguriert)
|
||||||
|
>>> run_esp32.main(host="192.168.1.115") # feste Adresse, statt der eingestellten
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Ohne `host` gilt die per MQTT gesetzte Adresse aus `settings.json`, sonst
|
||||||
|
`config.MOUNT_HOST` (siehe *Zwischen Mock und Montierung umschalten*).
|
||||||
|
|
||||||
### Dauerbetrieb: Autostart und Watchdog
|
### Dauerbetrieb: Autostart und Watchdog
|
||||||
|
|
||||||
`main.py` startet die Anzeige nach jedem Reset von selbst — nach Stromausfall
|
`main.py` startet die Anzeige nach jedem Reset von selbst — nach Stromausfall
|
||||||
@@ -301,6 +305,8 @@ Mac:
|
|||||||
- **Netzwerk zur echten Montierung.** Die GM4000 steht in `192.168.1.115`, das
|
- **Netzwerk zur echten Montierung.** Die GM4000 steht in `192.168.1.115`, das
|
||||||
Heimnetz des ESP32 ist `192.168.178.x` — der Mac erreicht sie per VPN, der
|
Heimnetz des ESP32 ist `192.168.178.x` — der Mac erreicht sie per VPN, der
|
||||||
ESP32 so nicht. Muss geklärt werden, bevor es an die echte Montierung geht.
|
ESP32 so nicht. Muss geklärt werden, bevor es an die echte Montierung geht.
|
||||||
|
Das *Umschalten* zwischen Mock und Montierung ist dagegen erledigt: Es geht
|
||||||
|
über MQTT (`set/mount_host`, siehe dort), ohne Deployen und ohne Neustart.
|
||||||
|
|
||||||
## Gradzeichen
|
## Gradzeichen
|
||||||
|
|
||||||
@@ -442,21 +448,25 @@ dazwischen fälschlich dunkel und wachte erst später auf.
|
|||||||
|
|
||||||
### Die Werte stehen in `settings.json`, nicht im Code
|
### Die Werte stehen in `settings.json`, nicht im Code
|
||||||
|
|
||||||
`config.py` ist Programmcode und ändert sich nur beim Deployen. Die Helligkeiten
|
`config.py` ist Programmcode und ändert sich nur beim Deployen. Die Helligkeiten,
|
||||||
und die Schwelle sollen dagegen im Betrieb verstellbar sein — **später über MQTT**.
|
die Schwelle und die Adresse der Montierung sollen dagegen im Betrieb verstellbar
|
||||||
Sie liegen deshalb als JSON auf dem Gerät und werden über `settings.py` gelesen und
|
sein — **über MQTT**. Sie liegen deshalb als JSON auf dem Gerät und werden über
|
||||||
geschrieben:
|
`settings.py` gelesen und geschrieben:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
import settings
|
import settings
|
||||||
settings.load() # mit Vorgaben aufgefüllt
|
settings.load() # mit Vorgaben aufgefüllt
|
||||||
settings.update({"hell_prozent": 60}) # ändern und speichern
|
settings.update({"hell_prozent": 60}) # ändern und speichern
|
||||||
|
settings.update({"mount_host": "192.168.1.115"}) # Montierung umstellen
|
||||||
```
|
```
|
||||||
|
|
||||||
Ein laufender `BrightnessController` übernimmt Änderungen mit `reload()`.
|
Ein laufender `BrightnessController` übernimmt Änderungen mit `reload()`.
|
||||||
|
|
||||||
Jeder Wert wird beim Laden **und** beim Schreiben geprüft (Typ und Bereich:
|
Jeder Wert wird beim Laden **und** beim Schreiben geprüft (Typ und Bereich:
|
||||||
Prozente 0–100, Schwelle 0–4095). Das ist der Grund, warum die Prüfung überhaupt
|
Prozente 0–100, Schwelle 0–4095, `mount_host` nur Zeichen aus IP-Adressen und
|
||||||
|
Hostnamen). Ob eine Einstellung Zahl oder Text ist, entscheidet der Typ ihrer
|
||||||
|
Vorgabe in `settings.DEFAULTS` — so gibt es nur eine Stelle, an der eine neue
|
||||||
|
Einstellung einzutragen ist. Das ist der Grund, warum die Prüfung überhaupt
|
||||||
existiert: Sobald MQTT dazukommt, kommen die Werte von außen, und ein
|
existiert: Sobald MQTT dazukommt, kommen die Werte von außen, und ein
|
||||||
Helligkeitswert von 500 % oder eine Schwelle von −1 würde die Anzeige sonst
|
Helligkeitswert von 500 % oder eine Schwelle von −1 würde die Anzeige sonst
|
||||||
unbrauchbar machen. Fehlt die Datei oder ist sie beschädigt, gelten die Vorgaben
|
unbrauchbar machen. Fehlt die Datei oder ist sie beschädigt, gelten die Vorgaben
|
||||||
@@ -529,7 +539,7 @@ darauf hin und kopiert sie nicht.
|
|||||||
|
|
||||||
### Topics
|
### Topics
|
||||||
|
|
||||||
Empfangen (`<PREFIX>/set/<name>`, Nutzlast als Zahl):
|
Empfangen (`<PREFIX>/set/<name>`):
|
||||||
|
|
||||||
| Topic | Beispiel | Wirkung |
|
| Topic | Beispiel | Wirkung |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
@@ -537,6 +547,10 @@ Empfangen (`<PREFIX>/set/<name>`, Nutzlast als Zahl):
|
|||||||
| `grossanzeige/set/dunkel_prozent` | `5` | Helligkeit unter der Schwelle |
|
| `grossanzeige/set/dunkel_prozent` | `5` | Helligkeit unter der Schwelle |
|
||||||
| `grossanzeige/set/schwelle` | `1800` | Umschaltpunkt (LDR-Wert) |
|
| `grossanzeige/set/schwelle` | `1800` | Umschaltpunkt (LDR-Wert) |
|
||||||
| `grossanzeige/set/hysterese` | `150` | Breite um den Umschaltpunkt |
|
| `grossanzeige/set/hysterese` | `150` | Breite um den Umschaltpunkt |
|
||||||
|
| `grossanzeige/set/mount_host` | `192.168.1.115` | Adresse der Montierung (leer = Vorgabe aus `config.py`) |
|
||||||
|
|
||||||
|
Alle Nutzlasten außer `mount_host` sind ganze Zahlen; `mount_host` ist Text
|
||||||
|
(IP-Adresse oder Hostname).
|
||||||
|
|
||||||
Gesendet (`<PREFIX>/status/<name>`, alle *retained*):
|
Gesendet (`<PREFIX>/status/<name>`, alle *retained*):
|
||||||
|
|
||||||
@@ -546,6 +560,7 @@ Gesendet (`<PREFIX>/status/<name>`, alle *retained*):
|
|||||||
| `grossanzeige/status/dec` | `+38°47'01"` |
|
| `grossanzeige/status/dec` | `+38°47'01"` |
|
||||||
| `grossanzeige/status/ldr` | `2453` |
|
| `grossanzeige/status/ldr` | `2453` |
|
||||||
| `grossanzeige/status/helligkeit` | `50` |
|
| `grossanzeige/status/helligkeit` | `50` |
|
||||||
|
| `grossanzeige/status/mount_host` | `192.168.1.115` (Adresse, die gerade abgefragt wird) |
|
||||||
| `grossanzeige/status/link` | `1` (Montierung erreichbar) |
|
| `grossanzeige/status/link` | `1` (Montierung erreichbar) |
|
||||||
| `grossanzeige/status/online` | `1` — **Last Will** setzt es auf `0` |
|
| `grossanzeige/status/online` | `1` — **Last Will** setzt es auf `0` |
|
||||||
|
|
||||||
@@ -566,6 +581,43 @@ raus — eine per MQTT gesetzte Helligkeit bliebe unsichtbar, solange die Stufe
|
|||||||
dieselbe bleibt. Das ist am Gerät aufgefallen (2026-07-27) und wird von
|
dieselbe bleibt. Das ist am Gerät aufgefallen (2026-07-27) und wird von
|
||||||
`test_mqtt.py` (`TestHelligkeitWirdSofortSichtbar`) festgehalten.
|
`test_mqtt.py` (`TestHelligkeitWirdSofortSichtbar`) festgehalten.
|
||||||
|
|
||||||
|
### Zwischen Mock und Montierung umschalten (`mount_host`)
|
||||||
|
|
||||||
|
Welche Adresse abgefragt wird, ist im Betrieb umstellbar — ohne Deployen, ohne
|
||||||
|
Neustart:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mosquitto_pub -h <broker> -t grossanzeige/set/mount_host -m 192.168.1.115 # echte GM4000
|
||||||
|
mosquitto_pub -h <broker> -t grossanzeige/set/mount_host -m 192.168.178.26 # Mock auf dem Mac
|
||||||
|
mosquitto_pub -h <broker> -t grossanzeige/set/mount_host -m '' # zurück zu config.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Die Adresse landet in `settings.json` und **übersteht damit einen Stromausfall**:
|
||||||
|
Nach dem Neustart fragt das Gerät dieselbe Adresse wieder ab. Welche gerade gilt,
|
||||||
|
klärt `mount_client.resolve_host()` in dieser Reihenfolge:
|
||||||
|
|
||||||
|
1. eine beim Aufruf ausdrücklich genannte Adresse (`run_esp32.main(host=...)` an
|
||||||
|
der REPL, `run_display.py --host` auf dem Mac),
|
||||||
|
2. `mount_host` aus `settings.json` (das per MQTT Gesetzte),
|
||||||
|
3. `config.MOUNT_HOST` — was beim Deployen im Code stand.
|
||||||
|
|
||||||
|
**Eine leere Nachricht ist der Weg zurück.** Ohne sie würde eine einmal gesetzte
|
||||||
|
Adresse die Vorgabe aus `config.py` für immer verdecken, auch nach einem Deployen
|
||||||
|
mit geändertem Code. Deshalb heißt „leer" hier nicht „keine Adresse", sondern
|
||||||
|
„nimm die aus dem Code".
|
||||||
|
|
||||||
|
Wirksam wird die Umstellung sofort: Der Rückruf trägt die neue Adresse in den
|
||||||
|
`MountClient` ein und **trennt die bestehende Verbindung** — neu verbunden wird im
|
||||||
|
nächsten Durchlauf der Poll-Schleife, mit der gewohnten Fehlerbehandlung. Antwortet
|
||||||
|
unter der neuen Adresse niemand, steht „KEIN LINK" auf der Anzeige, und eine weitere
|
||||||
|
Nachricht holt das Gerät wieder zurück. Was gerade abgefragt wird, meldet
|
||||||
|
`grossanzeige/status/mount_host` — die Rückmeldung kommt auch dann, wenn dort
|
||||||
|
niemand antwortet.
|
||||||
|
|
||||||
|
Der Wert geht in `socket.getaddrinfo` und kommt von außen, deshalb prüft
|
||||||
|
`settings.py` ihn eng: nur Buchstaben, Ziffern, Punkt und Bindestrich, höchstens
|
||||||
|
63 Zeichen. Alles andere wird verworfen, `settings.json` bleibt unberührt.
|
||||||
|
|
||||||
### Was dabei schiefgehen kann, und warum es nichts ausmacht
|
### Was dabei schiefgehen kann, und warum es nichts ausmacht
|
||||||
|
|
||||||
**Grundsatz: MQTT darf die Anzeige nie aufhalten.** Die Anzeige ist der Zweck des
|
**Grundsatz: MQTT darf die Anzeige nie aufhalten.** Die Anzeige ist der Zweck des
|
||||||
|
|||||||
@@ -123,6 +123,12 @@ DEGREE_FALLBACK = "d" # Ersatz, falls DEGREE_CHARSET = None
|
|||||||
# -- Montierung (Phase 2) -------------------------------------------------
|
# -- Montierung (Phase 2) -------------------------------------------------
|
||||||
# 10Micron GM4000 HPS II: LX200-Protokoll ueber TCP. Die IP der Montierung
|
# 10Micron GM4000 HPS II: LX200-Protokoll ueber TCP. Die IP der Montierung
|
||||||
# eintragen, sobald sie erreichbar ist; Port 3490 ist die Vorgabe von 10Micron.
|
# eintragen, sobald sie erreichbar ist; Port 3490 ist die Vorgabe von 10Micron.
|
||||||
|
#
|
||||||
|
# MOUNT_HOST ist nur die **Vorgabe**: Im Betrieb laesst sich die Adresse ueber
|
||||||
|
# MQTT umstellen (<PREFIX>/set/mount_host), sie liegt dann in settings.json und
|
||||||
|
# ueberlebt einen Stromausfall. Eine leere Nachricht an dieses Topic holt die
|
||||||
|
# Adresse von hier zurueck. Wer wissen will, was gerade gilt, liest
|
||||||
|
# <PREFIX>/status/mount_host oder ruft mount_client.resolve_host().
|
||||||
# Vorübergehend auf den Mock auf dem Mac gestellt (2026-07-27), weil die echte
|
# Vorübergehend auf den Mock auf dem Mac gestellt (2026-07-27), weil die echte
|
||||||
# Montierung vom ESP32 aus nicht erreichbar ist: Sie steht in 192.168.1.115, der
|
# Montierung vom ESP32 aus nicht erreichbar ist: Sie steht in 192.168.1.115, der
|
||||||
# ESP32 im Heimnetz 192.168.178.x. Zum Umstellen die beiden Zeilen tauschen.
|
# ESP32 im Heimnetz 192.168.178.x. Zum Umstellen die beiden Zeilen tauschen.
|
||||||
|
|||||||
+11
-1
@@ -52,7 +52,9 @@ def poll_loop(display, client, interval=None, once=False,
|
|||||||
"""Zyklisch die Montierung abfragen und die Anzeige nachfuehren.
|
"""Zyklisch die Montierung abfragen und die Anzeige nachfuehren.
|
||||||
|
|
||||||
display offenes Display-Objekt
|
display offenes Display-Objekt
|
||||||
client MountClient (wird bei Bedarf selbst verbunden)
|
client MountClient (wird bei Bedarf selbst verbunden). Seine Adresse
|
||||||
|
(client.host) darf sich zwischen zwei Durchlaeufen aendern --
|
||||||
|
die Schleife verbindet dann einfach neu.
|
||||||
interval Sekunden zwischen zwei Abfragen (Vorgabe config.POLL_INTERVAL)
|
interval Sekunden zwischen zwei Abfragen (Vorgabe config.POLL_INTERVAL)
|
||||||
once nur ein Durchlauf, dann zurueck
|
once nur ein Durchlauf, dann zurueck
|
||||||
read_ldr optional: Funktion ohne Argumente, liefert einen LDR-Rohwert
|
read_ldr optional: Funktion ohne Argumente, liefert einen LDR-Rohwert
|
||||||
@@ -65,6 +67,9 @@ def poll_loop(display, client, interval=None, once=False,
|
|||||||
bridge optional: mqtt.MqttBridge -- empfaengt Einstellungen und
|
bridge optional: mqtt.MqttBridge -- empfaengt Einstellungen und
|
||||||
veroeffentlicht den Zustand. Faellt der Broker aus, laeuft die
|
veroeffentlicht den Zustand. Faellt der Broker aus, laeuft die
|
||||||
Anzeige unveraendert weiter (mqtt.py faengt seine Fehler selbst).
|
Anzeige unveraendert weiter (mqtt.py faengt seine Fehler selbst).
|
||||||
|
Setzt eine Nachricht die Adresse der Montierung um, trennt der
|
||||||
|
Rueckruf die Verbindung (siehe run_esp32.main) -- noch in
|
||||||
|
diesem Durchlauf wird dann die neue Adresse angefragt.
|
||||||
feed optional: Funktion ohne Argumente, die den Watchdog fuettert.
|
feed optional: Funktion ohne Argumente, die den Watchdog fuettert.
|
||||||
Wird zu Beginn **jedes** Durchlaufs gerufen -- auch nach einem
|
Wird zu Beginn **jedes** Durchlaufs gerufen -- auch nach einem
|
||||||
Fehler, denn eine gemeldete Stoerung ist kein Stillstand: Die
|
Fehler, denn eine gemeldete Stoerung ist kein Stillstand: Die
|
||||||
@@ -91,6 +96,11 @@ def poll_loop(display, client, interval=None, once=False,
|
|||||||
# in diesem Durchlauf, statt eine Runde hinterherzuhinken.
|
# in diesem Durchlauf, statt eine Runde hinterherzuhinken.
|
||||||
if bridge is not None:
|
if bridge is not None:
|
||||||
bridge.check()
|
bridge.check()
|
||||||
|
# Gleich nach dem Empfang: Ein "set/mount_host" wirkt schon in
|
||||||
|
# diesem Durchlauf, und die Rueckmeldung zeigt, welche Adresse
|
||||||
|
# angekommen ist -- auch dann, wenn die Montierung dort nicht
|
||||||
|
# antwortet und der Rest der Runde in "KEIN LINK" endet.
|
||||||
|
bridge.publish("mount_host", client.host)
|
||||||
|
|
||||||
if ensure_link is not None:
|
if ensure_link is not None:
|
||||||
ensure_link()
|
ensure_link()
|
||||||
|
|||||||
@@ -2,17 +2,42 @@
|
|||||||
|
|
||||||
Die Socket-API (socket.socket/connect/send/recv) ist unter CPython und
|
Die Socket-API (socket.socket/connect/send/recv) ist unter CPython und
|
||||||
MicroPython gleich, laeuft also spaeter auch auf dem ESP32-C3.
|
MicroPython gleich, laeuft also spaeter auch auf dem ESP32-C3.
|
||||||
|
|
||||||
|
Welche Adresse abgefragt wird, klaert resolve_host(): per MQTT umstellbar,
|
||||||
|
mit config.MOUNT_HOST als Vorgabe.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import socket
|
import socket
|
||||||
|
|
||||||
|
import config
|
||||||
import lx200
|
import lx200
|
||||||
|
import settings
|
||||||
|
|
||||||
|
|
||||||
class MountError(Exception):
|
class MountError(Exception):
|
||||||
"""Verbindungs- oder Kommunikationsfehler mit der Montierung."""
|
"""Verbindungs- oder Kommunikationsfehler mit der Montierung."""
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_host(vorgabe=None, log=None):
|
||||||
|
"""Die Adresse liefern, die abgefragt werden soll.
|
||||||
|
|
||||||
|
Reihenfolge, absteigend:
|
||||||
|
|
||||||
|
1. ``vorgabe`` -- von Hand mitgegeben (REPL, Kommandozeile, Tests). Wer die
|
||||||
|
Adresse ausdruecklich nennt, meint sie auch.
|
||||||
|
2. ``mount_host`` aus settings.json -- per MQTT gesetzt und ueber einen
|
||||||
|
Stromausfall hinweg gespeichert.
|
||||||
|
3. ``config.MOUNT_HOST`` -- was beim Deployen im Code stand.
|
||||||
|
|
||||||
|
So laesst sich zwischen Mock und echter Montierung umschalten, ohne etwas
|
||||||
|
neu aufzuspielen, und ein leerer mount_host holt die Vorgabe aus dem Code
|
||||||
|
zurueck.
|
||||||
|
"""
|
||||||
|
if vorgabe:
|
||||||
|
return vorgabe
|
||||||
|
return settings.load(log=log).get("mount_host") or config.MOUNT_HOST
|
||||||
|
|
||||||
|
|
||||||
class MountClient:
|
class MountClient:
|
||||||
"""Fragt Koordinaten von der Montierung ab.
|
"""Fragt Koordinaten von der Montierung ab.
|
||||||
|
|
||||||
|
|||||||
@@ -4,11 +4,14 @@
|
|||||||
<PREFIX>/set/dunkel_prozent 5
|
<PREFIX>/set/dunkel_prozent 5
|
||||||
<PREFIX>/set/schwelle 1800
|
<PREFIX>/set/schwelle 1800
|
||||||
<PREFIX>/set/hysterese 150
|
<PREFIX>/set/hysterese 150
|
||||||
|
<PREFIX>/set/mount_host 192.168.1.115 Adresse der Montierung; leer =
|
||||||
|
Vorgabe aus config.MOUNT_HOST
|
||||||
|
|
||||||
<PREFIX>/status/ra 18h36m56s ausgehend
|
<PREFIX>/status/ra 18h36m56s ausgehend
|
||||||
<PREFIX>/status/dec +38°47'01"
|
<PREFIX>/status/dec +38°47'01"
|
||||||
<PREFIX>/status/ldr 2453
|
<PREFIX>/status/ldr 2453
|
||||||
<PREFIX>/status/helligkeit 50
|
<PREFIX>/status/helligkeit 50
|
||||||
|
<PREFIX>/status/mount_host 192.168.1.115 Adresse, die gerade abgefragt wird
|
||||||
<PREFIX>/status/link 1 Montierung erreichbar
|
<PREFIX>/status/link 1 Montierung erreichbar
|
||||||
<PREFIX>/status/online 1 retained, mit Last Will auf 0
|
<PREFIX>/status/online 1 retained, mit Last Will auf 0
|
||||||
|
|
||||||
@@ -149,11 +152,19 @@ class MqttBridge:
|
|||||||
if name not in SETZBAR:
|
if name not in SETZBAR:
|
||||||
self._log("MQTT: unbekannte Einstellung %r verworfen" % name)
|
self._log("MQTT: unbekannte Einstellung %r verworfen" % name)
|
||||||
return
|
return
|
||||||
try:
|
|
||||||
wert = int(roh)
|
# Zahl oder Text? Entscheidet der Typ der Vorgabe -- dieselbe Regel wie
|
||||||
except ValueError:
|
# in settings._pruefe. Eine leere Nachricht ist deshalb nur bei
|
||||||
self._log("MQTT: %s=%r ist keine ganze Zahl" % (name, roh))
|
# Textwerten sinnvoll: mount_host faellt damit auf config.MOUNT_HOST
|
||||||
return
|
# zurueck, waehrend int("") wie gehabt als Unsinn abgelehnt wird.
|
||||||
|
if isinstance(settings.DEFAULTS[name], str):
|
||||||
|
wert = roh
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
wert = int(roh)
|
||||||
|
except ValueError:
|
||||||
|
self._log("MQTT: %s=%r ist keine ganze Zahl" % (name, roh))
|
||||||
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
neu = settings.update({name: wert})
|
neu = settings.update({name: wert})
|
||||||
@@ -161,7 +172,7 @@ class MqttBridge:
|
|||||||
self._log("MQTT: %s" % e)
|
self._log("MQTT: %s" % e)
|
||||||
return
|
return
|
||||||
|
|
||||||
self._log("MQTT: %s = %d uebernommen" % (name, wert))
|
self._log("MQTT: %s = %r uebernommen" % (name, wert))
|
||||||
if self._on_change:
|
if self._on_change:
|
||||||
try:
|
try:
|
||||||
self._on_change(neu)
|
self._on_change(neu)
|
||||||
|
|||||||
@@ -16,8 +16,10 @@ USER = ""
|
|||||||
PASSWORD = ""
|
PASSWORD = ""
|
||||||
|
|
||||||
# Wurzel aller Topics. Darunter entstehen:
|
# Wurzel aller Topics. Darunter entstehen:
|
||||||
# <PREFIX>/set/<name> eingehend (hell_prozent, dunkel_prozent, schwelle, hysterese)
|
# <PREFIX>/set/<name> eingehend (hell_prozent, dunkel_prozent, schwelle,
|
||||||
# <PREFIX>/status/<name> ausgehend (ra, dec, ldr, helligkeit, link, online)
|
# hysterese, mount_host)
|
||||||
|
# <PREFIX>/status/<name> ausgehend (ra, dec, ldr, helligkeit, mount_host,
|
||||||
|
# link, online)
|
||||||
PREFIX = "grossanzeige"
|
PREFIX = "grossanzeige"
|
||||||
|
|
||||||
# Name des Geraets am Broker. Muss eindeutig sein, sonst werfen sich zwei
|
# Name des Geraets am Broker. Muss eindeutig sein, sonst werfen sich zwei
|
||||||
|
|||||||
+51
-27
@@ -28,7 +28,7 @@ import displayloop
|
|||||||
import wifi
|
import wifi
|
||||||
from brightness import BrightnessController, raw_to_ldr
|
from brightness import BrightnessController, raw_to_ldr
|
||||||
from display import open_display
|
from display import open_display
|
||||||
from mount_client import MountClient
|
from mount_client import MountClient, resolve_host
|
||||||
|
|
||||||
try: # MicroPython
|
try: # MicroPython
|
||||||
from machine import ADC, Pin
|
from machine import ADC, Pin
|
||||||
@@ -75,7 +75,7 @@ def selftest(host=None, port=None, log=print):
|
|||||||
cfg = wifi.connect(log=log)
|
cfg = wifi.connect(log=log)
|
||||||
log(" IP %s, Gateway %s" % (cfg[0], cfg[2]))
|
log(" IP %s, Gateway %s" % (cfg[0], cfg[2]))
|
||||||
|
|
||||||
host = config.MOUNT_HOST if host is None else host
|
host = resolve_host(host, log=log)
|
||||||
port = config.MOUNT_PORT if port is None else port
|
port = config.MOUNT_PORT if port is None else port
|
||||||
log("2) Montierung %s:%d ..." % (host, port))
|
log("2) Montierung %s:%d ..." % (host, port))
|
||||||
client = MountClient(host, port, timeout=config.MOUNT_TIMEOUT)
|
client = MountClient(host, port, timeout=config.MOUNT_TIMEOUT)
|
||||||
@@ -115,12 +115,55 @@ def make_watchdog(timeout=None, log=None):
|
|||||||
return wdt.feed
|
return wdt.feed
|
||||||
|
|
||||||
|
|
||||||
|
def make_on_change(display, client, dimmer=None, log=None):
|
||||||
|
"""Rueckruf fuer mqtt.MqttBridge: geaenderte Einstellungen sofort wirksam
|
||||||
|
machen. settings.json ist zu diesem Zeitpunkt schon geschrieben.
|
||||||
|
|
||||||
|
**Helligkeit:** Der Dimmer muss die neuen Werte nicht nur lesen -- die
|
||||||
|
Helligkeit muss auch neu an die Anzeige. Sonst bliebe eine gesetzte
|
||||||
|
Helligkeit unsichtbar, solange die Stufe dieselbe bleibt: ein Telegramm geht
|
||||||
|
sonst nur beim *Wechsel* raus. (Am Geraet aufgefallen, 2026-07-27.)
|
||||||
|
|
||||||
|
**Adresse der Montierung:** Die wirkt erst, wenn die bestehende Verbindung
|
||||||
|
faellt. Hier wird darum nur getrennt -- neu verbindet die Poll-Schleife im
|
||||||
|
naechsten Durchlauf von selbst, mit derselben Fehlerbehandlung wie sonst
|
||||||
|
auch: Antwortet unter der neuen Adresse niemand, steht "KEIN LINK", und ein
|
||||||
|
weiteres set/mount_host holt das Geraet wieder zurueck.
|
||||||
|
|
||||||
|
Eigene Funktion und nicht als Verschachtelung in main(), damit die Tests
|
||||||
|
genau diese Logik pruefen koennen statt einer Abschrift davon.
|
||||||
|
"""
|
||||||
|
log = log or (lambda *a: None)
|
||||||
|
|
||||||
|
def uebernehmen(werte):
|
||||||
|
try:
|
||||||
|
if dimmer is not None:
|
||||||
|
dimmer.reload()
|
||||||
|
if dimmer.level is not None:
|
||||||
|
display.set_brightness(dimmer.percent(dimmer.level))
|
||||||
|
else:
|
||||||
|
# Ohne LDR gibt es keine Stufen -- dann gilt "hell".
|
||||||
|
display.set_brightness(werte["hell_prozent"])
|
||||||
|
except Exception as e:
|
||||||
|
log("Anzeige: %s" % e)
|
||||||
|
|
||||||
|
neuer_host = resolve_host(werte.get("mount_host"))
|
||||||
|
if neuer_host != client.host:
|
||||||
|
log("Montierung: Adresse %s -> %s" % (client.host, neuer_host))
|
||||||
|
client.host = neuer_host
|
||||||
|
client.close()
|
||||||
|
|
||||||
|
return uebernehmen
|
||||||
|
|
||||||
|
|
||||||
def main(host=None, port=None, with_ldr=False, fake_display=False,
|
def main(host=None, port=None, with_ldr=False, fake_display=False,
|
||||||
once=False, with_mqtt=True, feed=None, log=print):
|
once=False, with_mqtt=True, feed=None, log=print):
|
||||||
"""WLAN aufbauen, Anzeige oeffnen und die Poll-Schleife fahren.
|
"""WLAN aufbauen, Anzeige oeffnen und die Poll-Schleife fahren.
|
||||||
|
|
||||||
host/port Montierung; Vorgabe aus config.py. Zum Testen gegen den Mock
|
host/port Montierung. Ohne host gilt die per MQTT gesetzte Adresse aus
|
||||||
auf dem Mac dessen LAN-Adresse angeben.
|
settings.json, sonst config.MOUNT_HOST (siehe
|
||||||
|
mount_client.resolve_host). Zum Testen gegen den Mock auf dem
|
||||||
|
Mac dessen LAN-Adresse angeben.
|
||||||
with_ldr Helligkeitsregelung ueber den ADC dazuschalten. Die Schwellen
|
with_ldr Helligkeitsregelung ueber den ADC dazuschalten. Die Schwellen
|
||||||
sind am realen LDR gemessen (2026-07-27, siehe brightness.py),
|
sind am realen LDR gemessen (2026-07-27, siehe brightness.py),
|
||||||
aber am Schreibtisch, nicht in der Sternwarte -- deshalb bleibt
|
aber am Schreibtisch, nicht in der Sternwarte -- deshalb bleibt
|
||||||
@@ -137,9 +180,10 @@ def main(host=None, port=None, with_ldr=False, fake_display=False,
|
|||||||
ihn an.
|
ihn an.
|
||||||
"""
|
"""
|
||||||
d = open_display(fake=fake_display)
|
d = open_display(fake=fake_display)
|
||||||
client = MountClient(config.MOUNT_HOST if host is None else host,
|
client = MountClient(resolve_host(host, log=log),
|
||||||
config.MOUNT_PORT if port is None else port,
|
config.MOUNT_PORT if port is None else port,
|
||||||
timeout=config.MOUNT_TIMEOUT)
|
timeout=config.MOUNT_TIMEOUT)
|
||||||
|
log("Montierung: %s:%d" % (client.host, client.port))
|
||||||
|
|
||||||
dimmer = read_ldr = None
|
dimmer = read_ldr = None
|
||||||
if with_ldr:
|
if with_ldr:
|
||||||
@@ -160,28 +204,8 @@ def main(host=None, port=None, with_ldr=False, fake_display=False,
|
|||||||
|
|
||||||
if with_mqtt:
|
if with_mqtt:
|
||||||
import mqtt
|
import mqtt
|
||||||
|
bridge = mqtt.connect_from_config(
|
||||||
def uebernehmen(werte):
|
log=log, on_change=make_on_change(d, client, dimmer, log=log))
|
||||||
"""Per MQTT geaenderte Einstellungen sofort sichtbar machen.
|
|
||||||
|
|
||||||
Der Dimmer muss die neuen Werte nicht nur lesen -- die Helligkeit
|
|
||||||
muss auch neu an die Anzeige. Sonst bliebe eine gesetzte
|
|
||||||
Helligkeit unsichtbar, solange die Stufe dieselbe bleibt: ein
|
|
||||||
Telegramm geht sonst nur beim *Wechsel* raus. (Am Geraet
|
|
||||||
aufgefallen, 2026-07-27.)
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
if dimmer is not None:
|
|
||||||
dimmer.reload()
|
|
||||||
if dimmer.level is not None:
|
|
||||||
d.set_brightness(dimmer.percent(dimmer.level))
|
|
||||||
else:
|
|
||||||
# Ohne LDR gibt es keine Stufen -- dann gilt "hell".
|
|
||||||
d.set_brightness(werte["hell_prozent"])
|
|
||||||
except Exception as e:
|
|
||||||
log("Anzeige: %s" % e)
|
|
||||||
|
|
||||||
bridge = mqtt.connect_from_config(log=log, on_change=uebernehmen)
|
|
||||||
|
|
||||||
displayloop.poll_loop(d, client,
|
displayloop.poll_loop(d, client,
|
||||||
once=once,
|
once=once,
|
||||||
|
|||||||
+56
-11
@@ -19,6 +19,10 @@ Alle Werte werden beim Laden UND beim Schreiben geprueft (siehe _pruefe). Das is
|
|||||||
wichtig, sobald MQTT dazukommt: Von dort kommen fremde Eingaben, und ein
|
wichtig, sobald MQTT dazukommt: Von dort kommen fremde Eingaben, und ein
|
||||||
Helligkeitswert von 500 % oder eine Schwelle von -1 wuerde die Anzeige sonst in
|
Helligkeitswert von 500 % oder eine Schwelle von -1 wuerde die Anzeige sonst in
|
||||||
einen unbrauchbaren Zustand bringen.
|
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
|
import json
|
||||||
@@ -35,10 +39,17 @@ DEFAULTS = {
|
|||||||
"hell_prozent": 50, # Helligkeit oberhalb der Schwelle
|
"hell_prozent": 50, # Helligkeit oberhalb der Schwelle
|
||||||
"schwelle": 1800, # LDR-Wert (nach raw_to_ldr) fuer den Umschaltpunkt
|
"schwelle": 1800, # LDR-Wert (nach raw_to_ldr) fuer den Umschaltpunkt
|
||||||
"hysterese": 150, # Breite um die Schwelle, gegen Flattern
|
"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 <PREFIX>/set/mount_host.
|
||||||
|
"mount_host": "",
|
||||||
}
|
}
|
||||||
|
|
||||||
# Erlaubte Bereiche: (minimum, maximum). Die Prozente gehen an ESC H, das nur
|
# Erlaubte Bereiche der Zahlenwerte: (minimum, maximum). Die Prozente gehen an
|
||||||
# 0..100 kennt; die LDR-Werte kommen aus einem 12-Bit-ADC.
|
# ESC H, das nur 0..100 kennt; die LDR-Werte kommen aus einem 12-Bit-ADC.
|
||||||
_GRENZEN = {
|
_GRENZEN = {
|
||||||
"dunkel_prozent": (0, 100),
|
"dunkel_prozent": (0, 100),
|
||||||
"hell_prozent": (0, 100),
|
"hell_prozent": (0, 100),
|
||||||
@@ -46,25 +57,59 @@ _GRENZEN = {
|
|||||||
"hysterese": (0, 2000),
|
"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):
|
class SettingsError(ValueError):
|
||||||
"""Ein Wert liegt ausserhalb des erlaubten Bereichs oder ist unbekannt."""
|
"""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):
|
def _pruefe(werte):
|
||||||
"""Werte auf Typ und Bereich pruefen. Liefert ein geprueftes neues dict."""
|
"""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 = {}
|
geprueft = {}
|
||||||
for name, wert in werte.items():
|
for name, wert in werte.items():
|
||||||
if name not in DEFAULTS:
|
if name not in DEFAULTS:
|
||||||
raise SettingsError("unbekannte Einstellung %r" % (name,))
|
raise SettingsError("unbekannte Einstellung %r" % (name,))
|
||||||
# bool ist in Python ein int -- hier aber sicher ein Irrtum.
|
if isinstance(DEFAULTS[name], str):
|
||||||
if isinstance(wert, bool) or not isinstance(wert, int):
|
geprueft[name] = _pruefe_text(name, wert)
|
||||||
raise SettingsError("%s: %r ist keine ganze Zahl" % (name, wert))
|
else:
|
||||||
low, high = _GRENZEN[name]
|
geprueft[name] = _pruefe_zahl(name, wert)
|
||||||
if not low <= wert <= high:
|
|
||||||
raise SettingsError("%s: %d liegt ausserhalb %d..%d"
|
|
||||||
% (name, wert, low, high))
|
|
||||||
geprueft[name] = wert
|
|
||||||
return geprueft
|
return geprueft
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -688,6 +688,36 @@ class TestEsp32GegenMock(unittest.TestCase):
|
|||||||
self.assertIn("TX: 02 81 80 81", hexdump) # STX, DA, SA, FC
|
self.assertIn("TX: 02 81 80 81", hexdump) # STX, DA, SA, FC
|
||||||
self.assertTrue(hexdump.strip().endswith("03")) # ETX
|
self.assertTrue(hexdump.strip().endswith("03")) # ETX
|
||||||
|
|
||||||
|
def test_gespeicherte_adresse_gilt_nach_dem_neustart(self):
|
||||||
|
"""Der Sinn des Speicherns: per MQTT gesetzt, Stromausfall, laeuft weiter.
|
||||||
|
|
||||||
|
main() ohne host nimmt, was in settings.json steht -- hier der Mock.
|
||||||
|
"""
|
||||||
|
import contextlib
|
||||||
|
import io
|
||||||
|
import os
|
||||||
|
import settings
|
||||||
|
|
||||||
|
pfad = "test_esp32_settings_tmp.json"
|
||||||
|
alt = settings.PFAD
|
||||||
|
settings.PFAD = pfad
|
||||||
|
try:
|
||||||
|
settings.update({"mount_host": self.mock.host})
|
||||||
|
with contextlib.redirect_stdout(io.StringIO()):
|
||||||
|
self.run_esp32.main(port=self.mock.port, fake_display=True,
|
||||||
|
once=True, with_mqtt=False,
|
||||||
|
log=self.log.append)
|
||||||
|
finally:
|
||||||
|
settings.PFAD = alt
|
||||||
|
try:
|
||||||
|
os.remove(pfad)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
text = "\n".join(self.log)
|
||||||
|
self.assertIn("Montierung: %s:%d" % (self.mock.host, self.mock.port), text)
|
||||||
|
self.assertIn("RA 18h36.9m", text) # der Mock hat wirklich geantwortet
|
||||||
|
|
||||||
def test_ohne_mock_zeigt_die_anzeige_kein_link(self):
|
def test_ohne_mock_zeigt_die_anzeige_kein_link(self):
|
||||||
"""Faellt die Gegenstelle aus, laeuft die Schleife weiter."""
|
"""Faellt die Gegenstelle aus, laeuft die Schleife weiter."""
|
||||||
import config
|
import config
|
||||||
|
|||||||
+46
-1
@@ -4,12 +4,15 @@
|
|||||||
python3 -m unittest -v test_lx200
|
python3 -m unittest -v test_lx200
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
|
import config
|
||||||
import coords
|
import coords
|
||||||
import lx200
|
import lx200
|
||||||
import mount_mock
|
import mount_mock
|
||||||
from mount_client import MountClient, MountError
|
import settings
|
||||||
|
from mount_client import MountClient, MountError, resolve_host
|
||||||
|
|
||||||
|
|
||||||
class TestLX200(unittest.TestCase):
|
class TestLX200(unittest.TestCase):
|
||||||
@@ -106,5 +109,47 @@ class TestClientFehler(unittest.TestCase):
|
|||||||
c.get_ra()
|
c.get_ra()
|
||||||
|
|
||||||
|
|
||||||
|
class TestAdresswahl(unittest.TestCase):
|
||||||
|
"""resolve_host: welche Adresse gilt -- Vorgabe, settings.json, config.py.
|
||||||
|
|
||||||
|
Die Adresse ist per MQTT umstellbar (Mock <-> Montierung) und liegt dann in
|
||||||
|
settings.json, ueberlebt also einen Stromausfall. Diese Reihenfolge ist der
|
||||||
|
Kern davon.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.pfad = "test_lx200_settings_tmp.json"
|
||||||
|
self._alt = settings.PFAD
|
||||||
|
settings.PFAD = self.pfad
|
||||||
|
self._weg()
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
settings.PFAD = self._alt
|
||||||
|
self._weg()
|
||||||
|
|
||||||
|
def _weg(self):
|
||||||
|
try:
|
||||||
|
os.remove(self.pfad)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def test_ohne_settings_gilt_config(self):
|
||||||
|
self.assertEqual(resolve_host(), config.MOUNT_HOST)
|
||||||
|
|
||||||
|
def test_gespeicherte_adresse_schlaegt_config(self):
|
||||||
|
settings.update({"mount_host": "192.168.1.115"})
|
||||||
|
self.assertEqual(resolve_host(), "192.168.1.115")
|
||||||
|
|
||||||
|
def test_leere_adresse_faellt_auf_config_zurueck(self):
|
||||||
|
settings.update({"mount_host": "192.168.1.115"})
|
||||||
|
settings.update({"mount_host": ""})
|
||||||
|
self.assertEqual(resolve_host(), config.MOUNT_HOST)
|
||||||
|
|
||||||
|
def test_ausdrueckliche_vorgabe_schlaegt_alles(self):
|
||||||
|
"""Wer beim Aufruf eine Adresse nennt, meint sie auch (REPL, Tests)."""
|
||||||
|
settings.update({"mount_host": "192.168.1.115"})
|
||||||
|
self.assertEqual(resolve_host("10.0.0.9"), "10.0.0.9")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main(verbosity=2)
|
unittest.main(verbosity=2)
|
||||||
|
|||||||
+148
-13
@@ -153,6 +153,31 @@ class TestEmpfang(MqttTest):
|
|||||||
self.assertEqual((w["schwelle"], w["dunkel_prozent"], w["hysterese"]),
|
self.assertEqual((w["schwelle"], w["dunkel_prozent"], w["hysterese"]),
|
||||||
(2000, 3, 100))
|
(2000, 3, 100))
|
||||||
|
|
||||||
|
def test_mount_host_wird_als_text_uebernommen(self):
|
||||||
|
"""Nicht jede Einstellung ist eine Zahl -- die Adresse ist Text."""
|
||||||
|
self.sende("ga/set/mount_host", "192.168.1.115")
|
||||||
|
self.assertEqual(settings.load(self.pfad)["mount_host"], "192.168.1.115")
|
||||||
|
self.assertEqual(self.geaendert[-1]["mount_host"], "192.168.1.115")
|
||||||
|
|
||||||
|
def test_mount_host_darf_ein_name_sein(self):
|
||||||
|
self.sende("ga/set/mount_host", "gm4000.sternwarte.local")
|
||||||
|
self.assertEqual(settings.load(self.pfad)["mount_host"],
|
||||||
|
"gm4000.sternwarte.local")
|
||||||
|
|
||||||
|
def test_leerer_mount_host_setzt_auf_die_vorgabe_zurueck(self):
|
||||||
|
"""Leer heisst: wieder config.MOUNT_HOST -- der Weg zurueck."""
|
||||||
|
self.sende("ga/set/mount_host", "10.0.0.9")
|
||||||
|
self.sende("ga/set/mount_host", "")
|
||||||
|
self.assertEqual(settings.load(self.pfad)["mount_host"], "")
|
||||||
|
|
||||||
|
def test_unsinnige_adresse_wird_abgelehnt(self):
|
||||||
|
"""Der Wert geht in socket.getaddrinfo -- da darf kein Muell ankommen."""
|
||||||
|
self.sende("ga/set/mount_host", "10.0.0.1; rm -rf /")
|
||||||
|
self.assertEqual(settings.load(self.pfad)["mount_host"],
|
||||||
|
settings.DEFAULTS["mount_host"])
|
||||||
|
self.assertEqual(self.geaendert, [])
|
||||||
|
self.assertTrue(any("unerlaubte" in m for m in self.meldungen))
|
||||||
|
|
||||||
def test_wert_ausserhalb_des_bereichs_wird_abgelehnt(self):
|
def test_wert_ausserhalb_des_bereichs_wird_abgelehnt(self):
|
||||||
"""Der Kern der Pruefung: von aussen darf nichts Unsinniges durch."""
|
"""Der Kern der Pruefung: von aussen darf nichts Unsinniges durch."""
|
||||||
self.sende("ga/set/hell_prozent", "500")
|
self.sende("ga/set/hell_prozent", "500")
|
||||||
@@ -272,6 +297,17 @@ class TestStoerungen(MqttTest):
|
|||||||
self.assertFalse(b.publish("ra", "x")) # wirft nicht
|
self.assertFalse(b.publish("ra", "x")) # wirft nicht
|
||||||
|
|
||||||
|
|
||||||
|
class FakeMountClient:
|
||||||
|
"""Nur so viel MountClient, wie der Rueckruf anfasst."""
|
||||||
|
|
||||||
|
def __init__(self, host="192.0.2.7"):
|
||||||
|
self.host = host
|
||||||
|
self.geschlossen = 0
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
self.geschlossen += 1
|
||||||
|
|
||||||
|
|
||||||
class TestHelligkeitWirdSofortSichtbar(MqttTest):
|
class TestHelligkeitWirdSofortSichtbar(MqttTest):
|
||||||
"""Am Geraet aufgefallen (2026-07-27): Einstellung uebernommen, Anzeige blieb.
|
"""Am Geraet aufgefallen (2026-07-27): Einstellung uebernommen, Anzeige blieb.
|
||||||
|
|
||||||
@@ -291,19 +327,11 @@ class TestHelligkeitWirdSofortSichtbar(MqttTest):
|
|||||||
dimmer.update(3000) # einschwingen auf HELL
|
dimmer.update(3000) # einschwingen auf HELL
|
||||||
return d, dimmer
|
return d, dimmer
|
||||||
|
|
||||||
def _uebernehmen_bauen(self, d, dimmer, log):
|
def _uebernehmen_bauen(self, d, dimmer, log, client=None):
|
||||||
"""Dieselbe Logik wie in run_esp32.main."""
|
"""Der echte Rueckruf aus run_esp32 -- keine Abschrift davon."""
|
||||||
def uebernehmen(werte):
|
import run_esp32
|
||||||
try:
|
return run_esp32.make_on_change(d, client or FakeMountClient(),
|
||||||
if dimmer is not None:
|
dimmer, log=log)
|
||||||
dimmer.reload()
|
|
||||||
if dimmer.level is not None:
|
|
||||||
d.set_brightness(dimmer.percent(dimmer.level))
|
|
||||||
else:
|
|
||||||
d.set_brightness(werte["hell_prozent"])
|
|
||||||
except Exception as e:
|
|
||||||
log("Anzeige: %s" % e)
|
|
||||||
return uebernehmen
|
|
||||||
|
|
||||||
def _helligkeiten(self, transport):
|
def _helligkeiten(self, transport):
|
||||||
"""Aus den Telegrammen die gesendeten Helligkeitswerte herausziehen."""
|
"""Aus den Telegrammen die gesendeten Helligkeitswerte herausziehen."""
|
||||||
@@ -340,6 +368,104 @@ class TestHelligkeitWirdSofortSichtbar(MqttTest):
|
|||||||
self.assertEqual(self._helligkeiten(d._t), [80])
|
self.assertEqual(self._helligkeiten(d._t), [80])
|
||||||
|
|
||||||
|
|
||||||
|
class TestAdresswechselWirktSofort(MqttTest):
|
||||||
|
"""Der Zweck der Uebung: zwischen Mock und Montierung umschalten.
|
||||||
|
|
||||||
|
Der Rueckruf traegt die neue Adresse in den Client ein und trennt die
|
||||||
|
bestehende Verbindung -- neu verbunden wird in der Poll-Schleife.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
import config
|
||||||
|
import run_esp32
|
||||||
|
from display import Display
|
||||||
|
from transport import FakeTransport
|
||||||
|
|
||||||
|
# Ausgangslage wie nach dem Start: der Client fragt die Adresse ab, die
|
||||||
|
# resolve_host geliefert hat -- hier die Vorgabe aus config.py.
|
||||||
|
self.mount = FakeMountClient(config.MOUNT_HOST)
|
||||||
|
self.display = Display(FakeTransport(echo=False), response=True)
|
||||||
|
self.bridge._on_change = run_esp32.make_on_change(
|
||||||
|
self.display, self.mount, None, log=self.meldungen.append)
|
||||||
|
self.bridge.connect()
|
||||||
|
|
||||||
|
def test_neue_adresse_landet_im_client(self):
|
||||||
|
self.sende("ga/set/mount_host", "192.168.1.115")
|
||||||
|
self.assertEqual(self.mount.host, "192.168.1.115")
|
||||||
|
|
||||||
|
def test_bestehende_verbindung_wird_getrennt(self):
|
||||||
|
"""Sonst liefe die Abfrage weiter zur alten Adresse."""
|
||||||
|
self.sende("ga/set/mount_host", "192.168.1.115")
|
||||||
|
self.assertEqual(self.mount.geschlossen, 1)
|
||||||
|
|
||||||
|
def test_dieselbe_adresse_trennt_nicht(self):
|
||||||
|
"""Eine wiederholte Nachricht darf die Abfrage nicht stoeren."""
|
||||||
|
self.sende("ga/set/mount_host", self.mount.host)
|
||||||
|
self.assertEqual(self.mount.geschlossen, 0)
|
||||||
|
|
||||||
|
def test_helligkeit_laesst_die_verbindung_in_ruhe(self):
|
||||||
|
import config
|
||||||
|
self.sende("ga/set/hell_prozent", "60")
|
||||||
|
self.assertEqual(self.mount.geschlossen, 0)
|
||||||
|
self.assertEqual(self.mount.host, config.MOUNT_HOST)
|
||||||
|
|
||||||
|
def test_leere_adresse_holt_die_vorgabe_aus_config(self):
|
||||||
|
import config
|
||||||
|
self.sende("ga/set/mount_host", "192.168.1.115")
|
||||||
|
self.sende("ga/set/mount_host", "")
|
||||||
|
self.assertEqual(self.mount.host, config.MOUNT_HOST)
|
||||||
|
self.assertEqual(self.mount.geschlossen, 2) # hin und zurueck
|
||||||
|
|
||||||
|
|
||||||
|
class TestUmschaltenImLaufendenBetrieb(MqttTest):
|
||||||
|
"""Dieselbe Kette wie am Geraet, nur mit echten Sockets:
|
||||||
|
Nachricht -> settings.json -> MountClient -> naechste Abfrage.
|
||||||
|
|
||||||
|
Angefangen wird an einer Adresse, an der niemand antwortet -- die Anzeige
|
||||||
|
steht auf "KEIN LINK". Nach dem set/mount_host holt die Poll-Schleife die
|
||||||
|
Koordinaten vom Mock, ohne dass jemand etwas neu startet.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
import run_esp32
|
||||||
|
from display import Display
|
||||||
|
from mount_client import MountClient
|
||||||
|
from mount_mock import MountMock
|
||||||
|
from transport import FakeTransport
|
||||||
|
|
||||||
|
self.mock = MountMock(host="127.0.0.1", port=0)
|
||||||
|
self.mock.start()
|
||||||
|
# 127.0.0.2 ist nicht belegt -- dort antwortet niemand. Kurzer Timeout,
|
||||||
|
# damit der erste Durchlauf den Test nicht aufhaelt.
|
||||||
|
self.mount = MountClient("127.0.0.2", self.mock.port, timeout=0.3)
|
||||||
|
self.display = Display(FakeTransport(echo=False), response=True)
|
||||||
|
self.bridge._on_change = run_esp32.make_on_change(
|
||||||
|
self.display, self.mount, None, log=self.meldungen.append)
|
||||||
|
self.bridge.connect()
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
self.mount.close()
|
||||||
|
self.mock.stop()
|
||||||
|
super().tearDown()
|
||||||
|
|
||||||
|
def _durchlauf(self):
|
||||||
|
import displayloop
|
||||||
|
return displayloop.poll_loop(self.display, self.mount, once=True,
|
||||||
|
bridge=self.bridge, log=lambda *a: None)
|
||||||
|
|
||||||
|
def test_von_kein_link_zur_montierung(self):
|
||||||
|
import config
|
||||||
|
self.assertEqual(self._durchlauf(), config.NOLINK_LINES)
|
||||||
|
|
||||||
|
self.sende("ga/set/mount_host", "127.0.0.1")
|
||||||
|
|
||||||
|
self.assertEqual(self._durchlauf()[0], "RA 18h36.9m")
|
||||||
|
self.assertIn(("ga/status/mount_host", b"127.0.0.1", True),
|
||||||
|
self.client.published)
|
||||||
|
|
||||||
|
|
||||||
class TestOhneKonfiguration(unittest.TestCase):
|
class TestOhneKonfiguration(unittest.TestCase):
|
||||||
|
|
||||||
def test_fehlende_mqtt_config_liefert_none(self):
|
def test_fehlende_mqtt_config_liefert_none(self):
|
||||||
@@ -369,6 +495,7 @@ class TestSchleifenanbindung(MqttTest):
|
|||||||
|
|
||||||
class Client:
|
class Client:
|
||||||
connected = True
|
connected = True
|
||||||
|
host = "192.0.2.7" # wird als status/mount_host gemeldet
|
||||||
|
|
||||||
def connect(self):
|
def connect(self):
|
||||||
pass
|
pass
|
||||||
@@ -391,6 +518,14 @@ class TestSchleifenanbindung(MqttTest):
|
|||||||
self.assertIn("ga/status/dec", self.topics())
|
self.assertIn("ga/status/dec", self.topics())
|
||||||
self.assertIn("ga/status/link", self.topics())
|
self.assertIn("ga/status/link", self.topics())
|
||||||
|
|
||||||
|
def test_abgefragte_adresse_wird_gemeldet(self):
|
||||||
|
"""Sonst bliebe offen, ob ein set/mount_host angekommen ist."""
|
||||||
|
self.bridge.connect()
|
||||||
|
self.client.published.clear()
|
||||||
|
self._loop_einmal(self.bridge)
|
||||||
|
self.assertIn(("ga/status/mount_host", b"192.0.2.7", True),
|
||||||
|
self.client.published)
|
||||||
|
|
||||||
def test_toter_broker_haelt_die_anzeige_nicht_auf(self):
|
def test_toter_broker_haelt_die_anzeige_nicht_auf(self):
|
||||||
"""Der wichtigste Test des Moduls."""
|
"""Der wichtigste Test des Moduls."""
|
||||||
self.bridge.connect()
|
self.bridge.connect()
|
||||||
|
|||||||
@@ -63,6 +63,19 @@ class TestLaden(Tempdatei):
|
|||||||
self.assertEqual(w["hell_prozent"], 42) # uebernommen
|
self.assertEqual(w["hell_prozent"], 42) # uebernommen
|
||||||
self.assertTrue(any("schwelle" in m for m in meldungen))
|
self.assertTrue(any("schwelle" in m for m in meldungen))
|
||||||
|
|
||||||
|
def test_kaputte_adresse_in_der_datei_verwirft_nur_diese(self):
|
||||||
|
self._schreibe_roh(json.dumps({"mount_host": "kein host!",
|
||||||
|
"hell_prozent": 42}))
|
||||||
|
meldungen = []
|
||||||
|
w = settings.load(self.pfad, log=meldungen.append)
|
||||||
|
self.assertEqual(w["mount_host"], settings.DEFAULTS["mount_host"])
|
||||||
|
self.assertEqual(w["hell_prozent"], 42)
|
||||||
|
|
||||||
|
def test_adresse_ueberlebt_das_speichern(self):
|
||||||
|
"""Der eigentliche Zweck: sie muss den Stromausfall ueberstehen."""
|
||||||
|
settings.update({"mount_host": "192.168.1.115"}, self.pfad)
|
||||||
|
self.assertEqual(settings.load(self.pfad)["mount_host"], "192.168.1.115")
|
||||||
|
|
||||||
def test_unbekannter_schluessel_wird_ignoriert(self):
|
def test_unbekannter_schluessel_wird_ignoriert(self):
|
||||||
self._schreibe_roh(json.dumps({"voellig_unbekannt": 1}))
|
self._schreibe_roh(json.dumps({"voellig_unbekannt": 1}))
|
||||||
w = settings.load(self.pfad)
|
w = settings.load(self.pfad)
|
||||||
@@ -122,6 +135,31 @@ class TestPruefung(unittest.TestCase):
|
|||||||
with self.assertRaises(settings.SettingsError):
|
with self.assertRaises(settings.SettingsError):
|
||||||
settings.save({"hell_prozent": wert}, "/dev/null")
|
settings.save({"hell_prozent": wert}, "/dev/null")
|
||||||
|
|
||||||
|
def test_adresse_wird_als_text_angenommen(self):
|
||||||
|
geprueft = settings._pruefe({"mount_host": "192.168.1.115"})
|
||||||
|
self.assertEqual(geprueft["mount_host"], "192.168.1.115")
|
||||||
|
|
||||||
|
def test_leere_adresse_ist_erlaubt(self):
|
||||||
|
"""Leer heisst 'Vorgabe aus config.py' -- der Weg zurueck zum Code."""
|
||||||
|
self.assertEqual(settings._pruefe({"mount_host": ""})["mount_host"], "")
|
||||||
|
|
||||||
|
def test_umgebende_leerzeichen_fallen_weg(self):
|
||||||
|
"""Bei MQTT-Nachrichten haengt schnell ein Zeilenende dran."""
|
||||||
|
self.assertEqual(settings._pruefe({"mount_host": " 10.0.0.5 "})["mount_host"],
|
||||||
|
"10.0.0.5")
|
||||||
|
|
||||||
|
def test_unerlaubte_zeichen_in_der_adresse(self):
|
||||||
|
"""Der Wert geht in socket.getaddrinfo -- nur Adressen und Namen."""
|
||||||
|
for wert in ("10.0.0.1 ; reboot", "hallo welt", "127.0.0.1:3490",
|
||||||
|
"192.168.1.5/24", "a" * 64):
|
||||||
|
with self.assertRaises(settings.SettingsError):
|
||||||
|
settings._pruefe({"mount_host": wert})
|
||||||
|
|
||||||
|
def test_adresse_muss_text_sein(self):
|
||||||
|
for wert in (42, None, True, ["10.0.0.1"]):
|
||||||
|
with self.assertRaises(settings.SettingsError):
|
||||||
|
settings._pruefe({"mount_host": wert})
|
||||||
|
|
||||||
def test_die_raender_sind_erlaubt(self):
|
def test_die_raender_sind_erlaubt(self):
|
||||||
geprueft = settings._pruefe({"hell_prozent": 100, "dunkel_prozent": 0,
|
geprueft = settings._pruefe({"hell_prozent": 100, "dunkel_prozent": 0,
|
||||||
"schwelle": 4095, "hysterese": 0})
|
"schwelle": 4095, "hysterese": 0})
|
||||||
|
|||||||
Reference in New Issue
Block a user