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>
156 lines
5.0 KiB
Python
156 lines
5.0 KiB
Python
#!/usr/bin/env python3
|
|
"""Tests fuer LX200-Protokoll, Mock und Client (Phase 2).
|
|
|
|
python3 -m unittest -v test_lx200
|
|
"""
|
|
|
|
import os
|
|
import unittest
|
|
|
|
import config
|
|
import coords
|
|
import lx200
|
|
import mount_mock
|
|
import settings
|
|
from mount_client import MountClient, MountError, resolve_host
|
|
|
|
|
|
class TestLX200(unittest.TestCase):
|
|
|
|
def test_command_rahmen(self):
|
|
self.assertEqual(lx200.command("GR"), ":GR#")
|
|
self.assertEqual(lx200.command(":GR#"), ":GR#")
|
|
self.assertEqual(lx200.command("GR#"), ":GR#")
|
|
self.assertEqual(lx200.command(":GR"), ":GR#")
|
|
|
|
def test_command_leer(self):
|
|
with self.assertRaises(lx200.LX200Error):
|
|
lx200.command(":#")
|
|
|
|
def test_strip_terminator(self):
|
|
self.assertEqual(lx200.strip_terminator("18:36:56#"), "18:36:56")
|
|
with self.assertRaises(lx200.LX200Error):
|
|
lx200.strip_terminator("18:36:56")
|
|
|
|
|
|
class TestAntwortParsen(unittest.TestCase):
|
|
"""Die LX200-Rohantworten muessen direkt in den coords-Parser passen."""
|
|
|
|
def test_ra_antwort(self):
|
|
self.assertEqual(coords.parse_ra("18:36:56"), (18, 36, 56))
|
|
|
|
def test_dec_antwort_mit_stern(self):
|
|
self.assertEqual(coords.parse_dec("+38*47:01"), ("+", 38, 47, 1))
|
|
|
|
def test_dec_antwort_mit_gradzeichen_0xDF(self):
|
|
# Meade-Stil: 0xDF als Gradzeichen
|
|
self.assertEqual(coords.parse_dec("-07\xdf15:30"), ("-", 7, 15, 30))
|
|
|
|
|
|
class TestMockFormat(unittest.TestCase):
|
|
|
|
def test_ra_roundtrip(self):
|
|
self.assertEqual(mount_mock._fmt_ra(mount_mock._parse_ra("18:36:56")), "18:36:56")
|
|
|
|
def test_dec_roundtrip(self):
|
|
s = mount_mock._parse_dec("-07*15:30")
|
|
self.assertEqual(mount_mock._fmt_dec(s), "-07*15:30")
|
|
|
|
def test_ra_wrap(self):
|
|
# 23:59:59 + 2s -> 00:00:01
|
|
self.assertEqual(mount_mock._fmt_ra(mount_mock._parse_ra("23:59:59") + 2), "00:00:01")
|
|
|
|
|
|
class TestClientGegenMock(unittest.TestCase):
|
|
"""Vollstaendiger Weg: Client <-> Mock ueber echtes TCP."""
|
|
|
|
def setUp(self):
|
|
self.mock = mount_mock.MountMock(host="127.0.0.1", port=0,
|
|
ra="18:36:56", dec="+38:47:01")
|
|
self.mock.start()
|
|
self.client = MountClient("127.0.0.1", self.mock.port, timeout=2.0)
|
|
self.client.connect()
|
|
|
|
def tearDown(self):
|
|
self.client.close()
|
|
self.mock.stop()
|
|
|
|
def test_get_ra(self):
|
|
self.assertEqual(self.client.get_ra(), "18:36:56")
|
|
|
|
def test_get_dec(self):
|
|
self.assertEqual(self.client.get_dec(), "+38*47:01")
|
|
|
|
def test_get_coordinates_und_formatierung(self):
|
|
ra_raw, dec_raw = self.client.get_coordinates()
|
|
self.assertEqual(coords.format_ra(ra_raw, high_precision=True), "18h36m56s")
|
|
# Seit CHARS_PER_LINE = 10 passt die volle Form mit abschliessendem ".
|
|
self.assertEqual(coords.format_dec(dec_raw, high_precision=True), "+38°47'01\"")
|
|
|
|
def test_produktname(self):
|
|
self.assertEqual(self.client.query(lx200.GET_PRODUCT), mount_mock.PRODUCT)
|
|
|
|
def test_unbekanntes_kommando_haengt_nicht(self):
|
|
# Muss eine (leere) abgeschlossene Antwort liefern, nicht blockieren.
|
|
self.assertEqual(self.client.query(":GXYZ#"), "")
|
|
|
|
|
|
class TestClientFehler(unittest.TestCase):
|
|
|
|
def test_verbindung_verweigert(self):
|
|
# Port, auf dem nichts lauscht
|
|
c = MountClient("127.0.0.1", 1, timeout=0.5)
|
|
with self.assertRaises(MountError):
|
|
c.connect()
|
|
|
|
def test_query_ohne_verbindung(self):
|
|
c = MountClient("127.0.0.1", 3490)
|
|
with self.assertRaises(MountError):
|
|
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__":
|
|
unittest.main(verbosity=2)
|