4461035777
Zwischen Mock und echter GM4000 umschalten, ohne zu deployen und ohne Neustart:
mosquitto_pub -t grossanzeige/set/mount_host -m 192.168.1.115
mosquitto_pub -t grossanzeige/set/mount_host -m 192.168.178.26
mosquitto_pub -t grossanzeige/set/mount_host -m ''
Die Adresse liegt in settings.json auf dem Geraet und uebersteht damit
Stromausfall und Watchdog-Neustart. deploy.sh kopiert die Datei bewusst nicht
mit, die Einstellung ueberlebt also auch ein Deployen.
**Leer heisst: config.MOUNT_HOST gilt.** mount_client.resolve_host loest auf in
der Reihenfolge: ausdruecklich mitgegebener Host (REPL, run_display --host) ->
mount_host aus settings.json -> config.MOUNT_HOST. Ohne diesen Rueckweg wuerde
eine einmal per MQTT gesetzte Adresse die Zeile in config.py fuer immer
verdecken, auch nach einem Deployen mit geaendertem Code.
Pruefung: settings._pruefe unterscheidet jetzt Zahl und Text am Typ der Vorgabe
in DEFAULTS -- eine neue Einstellung wird weiterhin an genau einer Stelle
eingetragen. Fuer mount_host sind nur Buchstaben, Ziffern, Punkt und
Bindestrich erlaubt, hoechstens 63 Zeichen: Der Wert geht ungeprueft in
socket.getaddrinfo, und von MQTT kommt alles Moegliche. Ungueltiges wird
verworfen, settings.json bleibt unberuehrt.
Sofort wirksam: Der Rueckruf traegt die neue Adresse in den MountClient ein und
trennt nur die bestehende Verbindung -- neu verbindet die Poll-Schleife im
naechsten Durchlauf, mit der gewohnten Fehlerbehandlung. Antwortet dort niemand,
steht "KEIN LINK", und eine weitere Nachricht holt das Geraet zurueck.
grossanzeige/status/mount_host meldet, welche Adresse gerade abgefragt wird --
die Rueckmeldung steht direkt hinter bridge.check() und nicht im Erfolgspfad,
kommt also auch dann, wenn die Montierung schweigt.
Der on_change-Rueckruf ist als run_esp32.make_on_change aus main() herausgeloest.
test_mqtt.py prueft ihn bisher als Abschrift ("Dieselbe Logik wie in
run_esp32.main"); jetzt laeuft der echte Code durch die Tests.
201 Tests gruen. Neu, unter anderem:
- TestUmschaltenImLaufendenBetrieb faehrt die ganze Kette an echten Sockets:
erst "KEIN LINK" an einer toten Adresse, nach dem set/mount_host die
Koordinaten vom Mock.
- TestAdresswahl (test_lx200) haelt die Reihenfolge von resolve_host fest.
- test_esp32: main() ohne host nimmt nach einem Neustart, was gespeichert ist.
Nicht angefasst: run_display.py auf dem Mac behaelt config.MOUNT_HOST als
Vorgabe fuer --host. Dort gibt es keine MQTT-Bruecke, und settings.json ist
Zustand des Geraets, nicht des Macs.
Am Geraet bestaetigt (2026-07-29), am Broker mitgeschnitten. Umschalten im
laufenden Betrieb, mit dem Mock auf dem Mac als Ausgangslage:
0.0s status/mount_host 192.168.178.26 Ausgangslage, link = 1
-> set/mount_host = 192.168.178.99
5.1s status/mount_host 192.168.178.99 Rueckmeldung
5.9s status/link 0 dort antwortet niemand
-> set/mount_host = ''
17.8s status/mount_host 192.168.178.26 Vorgabe aus config.py
18.6s status/link 1 Koordinaten laufen wieder
Und ueber den Neustart hinweg -- der eigentliche Zweck des Speicherns:
-> set/mount_host = 192.168.178.99
1.6s status/mount_host 192.168.178.99
settings.json auf dem Geraet:
{"hysterese": 150, "mount_host": "192.168.178.99", "schwelle": 1800, ...}
-> Hardware-Reset
8.6s status/online 0 Last Will
15.5s status/online 1 Autostart durch
15.5s status/mount_host 192.168.178.99 aus der Datei, nicht aus config.py
Die alte settings.json ohne mount_host wird beim Laden einfach um die Vorgabe
ergaenzt; die eingestellten Helligkeiten bleiben unberuehrt. Eine Migration war
nicht noetig.
set/mount_host wird ohne Retain-Flag geschickt: Das ist ein Befehl, kein
Zustand. Retained laege er im Broker und wuerde bei jedem Reconnect erneut
zugestellt.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
542 lines
20 KiB
Python
542 lines
20 KiB
Python
#!/usr/bin/env python3
|
|
"""Tests fuer die MQTT-Anbindung.
|
|
|
|
python3 -m unittest -v test_mqtt
|
|
|
|
Statt eines echten Brokers wird ein FakeClient eingehaengt, der dieselbe
|
|
Schnittstelle wie umqtt.simple.MQTTClient bietet. So laesst sich alles pruefen,
|
|
was im Betrieb schiefgehen kann -- auch die Faelle, die man mit einem echten
|
|
Broker nur schwer herbeifuehrt: Verbindungsabbruch beim Senden, Muell im Topic,
|
|
ein Broker, der gar nicht antwortet.
|
|
|
|
Schwerpunkt ist der Grundsatz aus mqtt.py: **MQTT darf die Anzeige nie
|
|
aufhalten.** Kein Test hier darf eine Ausnahme nach aussen durchlassen.
|
|
"""
|
|
|
|
import os
|
|
import unittest
|
|
|
|
import mqtt
|
|
import settings
|
|
|
|
|
|
class FakeClient:
|
|
"""Ersatz fuer umqtt.simple.MQTTClient."""
|
|
|
|
def __init__(self, **kw):
|
|
self.published = [] # (topic, payload, retain)
|
|
self.subscribed = []
|
|
self.last_will = None
|
|
self.callback = None
|
|
self.connected = False
|
|
self.disconnected = 0
|
|
self.eingang = [] # (topic, payload) -- werden bei check_msg zugestellt
|
|
self.fail_connect = False
|
|
self.fail_publish = False
|
|
self.fail_check = False
|
|
|
|
def set_callback(self, f):
|
|
self.callback = f
|
|
|
|
def set_last_will(self, topic, msg, retain=False):
|
|
self.last_will = (topic, msg, retain)
|
|
|
|
def connect(self):
|
|
if self.fail_connect:
|
|
raise OSError("Broker nicht erreichbar")
|
|
self.connected = True
|
|
|
|
def subscribe(self, topic):
|
|
self.subscribed.append(topic)
|
|
|
|
def publish(self, topic, msg, retain=False):
|
|
if self.fail_publish:
|
|
raise OSError("Verbindung weg")
|
|
self.published.append((topic, msg, retain))
|
|
|
|
def check_msg(self):
|
|
if self.fail_check:
|
|
raise OSError("Verbindung weg")
|
|
while self.eingang:
|
|
topic, payload = self.eingang.pop(0)
|
|
if self.callback:
|
|
self.callback(topic, payload)
|
|
|
|
def disconnect(self):
|
|
self.disconnected += 1
|
|
self.connected = False
|
|
|
|
|
|
class MqttTest(unittest.TestCase):
|
|
"""Basis: eine Bruecke mit FakeClient und eigener settings-Datei."""
|
|
|
|
def setUp(self):
|
|
self.pfad = "test_mqtt_settings_tmp.json"
|
|
self._weg()
|
|
# settings.PFAD umbiegen, damit die Tests nicht die echte Datei anfassen.
|
|
self._alt = settings.PFAD
|
|
settings.PFAD = self.pfad
|
|
settings.save(settings.DEFAULTS, self.pfad)
|
|
|
|
self.meldungen = []
|
|
self.geaendert = []
|
|
self.client = FakeClient()
|
|
self.bridge = mqtt.MqttBridge(
|
|
broker="test", prefix="ga", client=self.client,
|
|
log=self.meldungen.append,
|
|
on_change=self.geaendert.append)
|
|
|
|
def tearDown(self):
|
|
settings.PFAD = self._alt
|
|
self._weg()
|
|
|
|
def _weg(self):
|
|
try:
|
|
os.remove(self.pfad)
|
|
except OSError:
|
|
pass
|
|
|
|
def sende(self, topic, payload):
|
|
"""Eine Nachricht vom Broker zustellen lassen."""
|
|
self.client.eingang.append((topic.encode(), payload.encode()))
|
|
self.bridge.check()
|
|
|
|
def topics(self):
|
|
return [t.decode() if isinstance(t, bytes) else t
|
|
for t, _, _ in self.client.published]
|
|
|
|
|
|
class TestVerbinden(MqttTest):
|
|
|
|
def test_connect_abonniert_und_meldet_online(self):
|
|
self.assertTrue(self.bridge.connect())
|
|
self.assertTrue(self.bridge.connected)
|
|
self.assertEqual(self.client.subscribed, ["ga/set/+"])
|
|
self.assertIn(("ga/status/online", b"1", True), self.client.published)
|
|
|
|
def test_last_will_meldet_offline(self):
|
|
"""Bricht die Verbindung weg, muss der Broker selbst offline melden."""
|
|
self.bridge.connect()
|
|
self.assertEqual(self.client.last_will, ("ga/status/online", b"0", True))
|
|
|
|
def test_fehlgeschlagene_verbindung_wirft_nicht(self):
|
|
self.client.fail_connect = True
|
|
self.assertFalse(self.bridge.connect())
|
|
self.assertFalse(self.bridge.connected)
|
|
self.assertTrue(any("fehlgeschlagen" in m for m in self.meldungen))
|
|
|
|
def test_close_meldet_offline_ab(self):
|
|
self.bridge.connect()
|
|
self.bridge.close()
|
|
self.assertIn(("ga/status/online", b"0", True), self.client.published)
|
|
self.assertFalse(self.bridge.connected)
|
|
|
|
|
|
class TestEmpfang(MqttTest):
|
|
|
|
def setUp(self):
|
|
super().setUp()
|
|
self.bridge.connect()
|
|
|
|
def test_gueltiger_wert_wird_uebernommen(self):
|
|
self.sende("ga/set/hell_prozent", "60")
|
|
self.assertEqual(settings.load(self.pfad)["hell_prozent"], 60)
|
|
self.assertEqual(self.geaendert[-1]["hell_prozent"], 60)
|
|
|
|
def test_alle_setzbaren_werte(self):
|
|
for name in mqtt.SETZBAR:
|
|
self.assertIn(name, settings.DEFAULTS)
|
|
self.sende("ga/set/schwelle", "2000")
|
|
self.sende("ga/set/dunkel_prozent", "3")
|
|
self.sende("ga/set/hysterese", "100")
|
|
w = settings.load(self.pfad)
|
|
self.assertEqual((w["schwelle"], w["dunkel_prozent"], w["hysterese"]),
|
|
(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):
|
|
"""Der Kern der Pruefung: von aussen darf nichts Unsinniges durch."""
|
|
self.sende("ga/set/hell_prozent", "500")
|
|
self.assertEqual(settings.load(self.pfad)["hell_prozent"],
|
|
settings.DEFAULTS["hell_prozent"])
|
|
self.assertEqual(self.geaendert, [])
|
|
self.assertTrue(any("500" in m for m in self.meldungen))
|
|
|
|
def test_keine_zahl_wird_abgelehnt(self):
|
|
self.sende("ga/set/hell_prozent", "hell bitte")
|
|
self.assertEqual(settings.load(self.pfad)["hell_prozent"],
|
|
settings.DEFAULTS["hell_prozent"])
|
|
self.assertTrue(any("ganze Zahl" in m for m in self.meldungen))
|
|
|
|
def test_unbekanntes_topic_wird_verworfen(self):
|
|
self.sende("ga/set/gibtsnicht", "1")
|
|
self.assertEqual(self.geaendert, [])
|
|
self.assertTrue(any("unbekannt" in m for m in self.meldungen))
|
|
|
|
def test_leere_nachricht_wirft_nicht(self):
|
|
self.sende("ga/set/hell_prozent", "")
|
|
self.assertEqual(self.geaendert, [])
|
|
|
|
def test_kaputtes_on_change_bricht_nichts_ab(self):
|
|
"""Ein Fehler im Rueckruf darf den Empfang nicht toeten."""
|
|
def kaputt(_):
|
|
raise RuntimeError("absichtlich")
|
|
self.bridge._on_change = kaputt
|
|
self.sende("ga/set/hell_prozent", "60") # darf nicht werfen
|
|
self.assertEqual(settings.load(self.pfad)["hell_prozent"], 60)
|
|
self.assertTrue(any("on_change" in m for m in self.meldungen))
|
|
|
|
|
|
class TestSenden(MqttTest):
|
|
|
|
def setUp(self):
|
|
super().setUp()
|
|
self.bridge.connect()
|
|
self.client.published.clear()
|
|
|
|
def test_status_wird_veroeffentlicht(self):
|
|
self.bridge.publish("ra", "18h36m56s")
|
|
self.assertIn(("ga/status/ra", b"18h36m56s", True), self.client.published)
|
|
|
|
def test_gleicher_wert_wird_nicht_erneut_gesendet(self):
|
|
"""Spart Funkverkehr: LDR und Helligkeit aendern sich kaum."""
|
|
self.bridge.publish("ldr", 2453)
|
|
self.bridge.publish("ldr", 2453)
|
|
self.assertEqual(self.topics().count("ga/status/ldr"), 1)
|
|
|
|
def test_geaenderter_wert_wird_gesendet(self):
|
|
self.bridge.publish("ldr", 2453)
|
|
self.bridge.publish("ldr", 2600)
|
|
self.assertEqual(self.topics().count("ga/status/ldr"), 2)
|
|
|
|
def test_publish_many(self):
|
|
self.bridge.publish_many({"ra": "1", "dec": "2"})
|
|
self.assertIn("ga/status/ra", self.topics())
|
|
self.assertIn("ga/status/dec", self.topics())
|
|
|
|
def test_nach_reconnect_wird_alles_neu_gesendet(self):
|
|
"""Sonst fehlte dem Broker nach einem Abriss der aktuelle Stand."""
|
|
self.bridge.publish("ldr", 2453)
|
|
self.bridge._abwerfen()
|
|
self.bridge._client = self.client
|
|
self.bridge._naechster_versuch = None
|
|
self.bridge.connect()
|
|
self.client.published.clear()
|
|
self.bridge.publish("ldr", 2453)
|
|
self.assertIn("ga/status/ldr", self.topics())
|
|
|
|
|
|
class TestStoerungen(MqttTest):
|
|
"""Der eigentliche Zweck: die Anzeige darf nie stehenbleiben."""
|
|
|
|
def test_senden_bei_toter_verbindung_wirft_nicht(self):
|
|
self.bridge.connect()
|
|
self.client.fail_publish = True
|
|
self.assertFalse(self.bridge.publish("ra", "x"))
|
|
self.assertFalse(self.bridge.connected)
|
|
|
|
def test_empfang_bei_toter_verbindung_wirft_nicht(self):
|
|
self.bridge.connect()
|
|
self.client.fail_check = True
|
|
self.assertFalse(self.bridge.check())
|
|
self.assertFalse(self.bridge.connected)
|
|
|
|
def test_ohne_verbindung_wird_nicht_bei_jedem_aufruf_versucht(self):
|
|
"""Sonst kostet ein toter Broker in jedem Schleifendurchlauf Zeit."""
|
|
b = mqtt.MqttBridge(broker="test", prefix="ga", client=FakeClient(),
|
|
log=self.meldungen.append)
|
|
b._client.fail_connect = True
|
|
self.assertFalse(b.connect())
|
|
vorher = len(self.meldungen)
|
|
for _ in range(5):
|
|
self.assertFalse(b.check())
|
|
# Kein einziger neuer Versuch, solange die Wartezeit laeuft.
|
|
self.assertEqual(len(self.meldungen), vorher)
|
|
|
|
def test_wartezeit_waechst_und_ist_begrenzt(self):
|
|
b = mqtt.MqttBridge(broker="test", prefix="ga", client=FakeClient())
|
|
b._client.fail_connect = True
|
|
werte = []
|
|
for _ in range(12):
|
|
b._client = FakeClient()
|
|
b._client.fail_connect = True
|
|
b._naechster_versuch = None
|
|
b.connect()
|
|
werte.append(b._retry)
|
|
self.assertGreater(werte[1], werte[0]) # waechst
|
|
self.assertLessEqual(max(werte), mqtt.RETRY_MAX) # aber begrenzt
|
|
|
|
def test_publish_ohne_verbindung_meldet_nur_false(self):
|
|
b = mqtt.MqttBridge(broker="test", prefix="ga", client=FakeClient())
|
|
b._client.fail_connect = True
|
|
b.connect()
|
|
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):
|
|
"""Am Geraet aufgefallen (2026-07-27): Einstellung uebernommen, Anzeige blieb.
|
|
|
|
Ein Helligkeitstelegramm geht nur beim *Wechsel* der Stufe raus. Wird per
|
|
MQTT hell_prozent geaendert, die Stufe bleibt aber dieselbe, muss die
|
|
Helligkeit trotzdem neu gesetzt werden -- sonst wirkt die Aenderung erst
|
|
beim naechsten zufaelligen Stufenwechsel.
|
|
"""
|
|
|
|
def _dimmer_und_display(self):
|
|
import brightness
|
|
from display import Display
|
|
from transport import FakeTransport
|
|
|
|
d = Display(FakeTransport(echo=False), response=True)
|
|
dimmer = brightness.BrightnessController(pfad=self.pfad)
|
|
dimmer.update(3000) # einschwingen auf HELL
|
|
return d, dimmer
|
|
|
|
def _uebernehmen_bauen(self, d, dimmer, log, client=None):
|
|
"""Der echte Rueckruf aus run_esp32 -- keine Abschrift davon."""
|
|
import run_esp32
|
|
return run_esp32.make_on_change(d, client or FakeMountClient(),
|
|
dimmer, log=log)
|
|
|
|
def _helligkeiten(self, transport):
|
|
"""Aus den Telegrammen die gesendeten Helligkeitswerte herausziehen."""
|
|
out = []
|
|
for frame in transport.frames:
|
|
i = frame.find(b"\x1bH")
|
|
if i >= 0:
|
|
out.append(int(frame[i + 3:i + 6]))
|
|
return out
|
|
|
|
def test_geaenderte_helligkeit_geht_an_die_anzeige(self):
|
|
d, dimmer = self._dimmer_und_display()
|
|
self.bridge._on_change = self._uebernehmen_bauen(d, dimmer,
|
|
self.meldungen.append)
|
|
self.bridge.connect()
|
|
d._t.frames.clear()
|
|
|
|
self.sende("ga/set/hell_prozent", "100")
|
|
|
|
self.assertEqual(self._helligkeiten(d._t), [100],
|
|
"Helligkeit wurde nicht an die Anzeige gesendet")
|
|
|
|
def test_ohne_dimmer_gilt_die_helle_stufe(self):
|
|
from display import Display
|
|
from transport import FakeTransport
|
|
d = Display(FakeTransport(echo=False), response=True)
|
|
self.bridge._on_change = self._uebernehmen_bauen(d, None,
|
|
self.meldungen.append)
|
|
self.bridge.connect()
|
|
d._t.frames.clear()
|
|
|
|
self.sende("ga/set/hell_prozent", "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):
|
|
|
|
def test_fehlende_mqtt_config_liefert_none(self):
|
|
"""Ohne mqtt_config.py laeuft die Anzeige ohne MQTT weiter."""
|
|
import sys
|
|
gemerkt = sys.modules.pop("mqtt_config", None)
|
|
pfad = sys.path[:]
|
|
try:
|
|
# Sicherstellen, dass mqtt_config wirklich nicht importierbar ist.
|
|
sys.path = [p for p in sys.path if p not in ("", ".", os.getcwd())]
|
|
meldungen = []
|
|
self.assertIsNone(mqtt.connect_from_config(log=meldungen.append))
|
|
self.assertTrue(any("mqtt_config" in m for m in meldungen))
|
|
finally:
|
|
sys.path = pfad
|
|
if gemerkt is not None:
|
|
sys.modules["mqtt_config"] = gemerkt
|
|
|
|
|
|
class TestSchleifenanbindung(MqttTest):
|
|
"""poll_loop mit Bruecke -- die Anzeige laeuft, auch wenn MQTT streikt."""
|
|
|
|
def _loop_einmal(self, bridge):
|
|
import displayloop
|
|
from display import Display
|
|
from transport import FakeTransport
|
|
|
|
class Client:
|
|
connected = True
|
|
host = "192.0.2.7" # wird als status/mount_host gemeldet
|
|
|
|
def connect(self):
|
|
pass
|
|
|
|
def close(self):
|
|
pass
|
|
|
|
def get_coordinates(self):
|
|
return "18:36:56", "+38*47:01"
|
|
|
|
d = Display(FakeTransport(echo=False), response=True)
|
|
return displayloop.poll_loop(d, Client(), once=True, bridge=bridge,
|
|
log=lambda *a: None)
|
|
|
|
def test_koordinaten_gehen_raus(self):
|
|
self.bridge.connect()
|
|
self.client.published.clear()
|
|
self._loop_einmal(self.bridge)
|
|
self.assertIn("ga/status/ra", self.topics())
|
|
self.assertIn("ga/status/dec", 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):
|
|
"""Der wichtigste Test des Moduls."""
|
|
self.bridge.connect()
|
|
self.client.fail_publish = True
|
|
self.client.fail_check = True
|
|
gezeigt = self._loop_einmal(self.bridge)
|
|
self.assertIsNotNone(gezeigt) # Anzeige wurde trotzdem bedient
|
|
# Bei Niedrigpraezision (config-Vorgabe) die beschriftete Zehntelform.
|
|
self.assertEqual(gezeigt[0], "RA 18h36.9m")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main(verbosity=2)
|