OTA-Update per MQTT

Neues Kommando <PREFIX>/cmd/ota loest ota.update() aus: laedt die
Projektdateien per HTTP, erst als temporaere .ota-Dateien, und benennt
sie nur bei vollem Erfolg um. Schlaegt ein Download fehl, bleiben die
bisherigen Dateien unveraendert -- die Anzeige laeuft mit der alten
Version weiter (Grundsatz wie beim Rest von MQTT: eine fehlgeschlagene
OTA darf das Geraet nicht unbrauchbar machen).

Am Gerät bestätigt, inkl. echtem Reboot: leere/ungueltige Payload,
unbekanntes Kommando und nicht erreichbarer Server werden abgefangen,
die Poll-Schleife laeuft unbeeinflusst weiter. Ein echtes Update mit
allen Projektdateien kam sauber ueber main.py-Autostart wieder hoch.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017zTSXLK4w32PoCmFgSzp68
This commit is contained in:
2026-09-04 17:34:58 +02:00
parent 72da766af0
commit 0e4bb025ef
9 changed files with 534 additions and 9 deletions
+50 -1
View File
@@ -111,7 +111,7 @@ 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.assertEqual(self.client.subscribed, ["ga/set/+", "ga/cmd/+"])
self.assertIn(("ga/status/online", b"1", True), self.client.published)
def test_last_will_meldet_offline(self):
@@ -537,5 +537,54 @@ class TestSchleifenanbindung(MqttTest):
self.assertEqual(gezeigt[0], "RA 18h36.9m")
class TestOta(MqttTest):
"""cmd/ota loest den injizierten on_ota-Rueckruf aus (siehe ota.py,
run_esp32.make_on_ota). Die eigentliche OTA-Logik wird hier nicht
getestet -- nur, dass mqtt.py sie zuverlaessig anstoesst, ohne dabei die
Anzeige mitzureissen."""
def setUp(self):
super().setUp()
self.ota_aufrufe = []
self.bridge._on_ota = self.ota_aufrufe.append
self.bridge.connect()
def test_cmd_topic_wird_abonniert(self):
self.assertIn("ga/cmd/+", self.client.subscribed)
def test_ota_wird_mit_payload_ausgeloest(self):
self.sende("ga/cmd/ota", "http://192.168.1.50:8000")
self.assertEqual(self.ota_aufrufe, ["http://192.168.1.50:8000"])
def test_ota_ohne_payload_wird_trotzdem_ausgeloest(self):
"""Leere Nachricht heisst: config.OTA_URL gilt (make_on_ota)."""
self.sende("ga/cmd/ota", "")
self.assertEqual(self.ota_aufrufe, [""])
def test_unbekanntes_kommando_wird_verworfen(self):
self.sende("ga/cmd/reboot", "")
self.assertEqual(self.ota_aufrufe, [])
self.assertTrue(any("unbekanntes Kommando" in m for m in self.meldungen))
def test_ohne_handler_wird_nur_protokolliert(self):
self.bridge._on_ota = None
self.sende("ga/cmd/ota", "http://x")
self.assertTrue(any("kein Handler" in m for m in self.meldungen))
def test_kaputter_on_ota_bricht_nichts_ab(self):
"""Ein Fehler beim OTA-Anstossen darf den Empfang nicht toeten."""
def kaputt(_):
raise RuntimeError("absichtlich")
self.bridge._on_ota = kaputt
self.sende("ga/cmd/ota", "http://x") # darf nicht werfen
self.assertTrue(any("OTA fehlgeschlagen" in m for m in self.meldungen))
def test_set_topics_bleiben_unberuehrt(self):
"""Das gruppenbasierte Routing (set/cmd) darf set/ nicht veraendern."""
self.sende("ga/set/hell_prozent", "60")
self.assertEqual(settings.load(self.pfad)["hell_prozent"], 60)
self.assertEqual(self.ota_aufrufe, [])
if __name__ == "__main__":
unittest.main(verbosity=2)