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
+57 -2
View File
@@ -460,6 +460,61 @@ class TestWatchdogInDerSchleife(unittest.TestCase):
self.assertEqual(len(self._lauf(Client())), 1)
class TestOnOta(unittest.TestCase):
"""run_esp32.make_on_ota: baut den MQTT-Rueckruf fuer cmd/ota.
ota.update() selbst wird hier durch eine Attrappe ersetzt -- getestet
wird nur, ob make_on_ota die richtige URL und den Watchdog-Feed
durchreicht, nicht der Download (siehe test_ota.py dafuer).
"""
def setUp(self):
import ota
import run_esp32
self.ota = ota
self.run_esp32 = run_esp32
self.aufrufe = []
def fake_update(url, files=None, log=None, feed=None, getter=None):
self.aufrufe.append((url, feed))
self._alt_update = ota.update
ota.update = fake_update
self.addCleanup(lambda: setattr(ota, "update", self._alt_update))
import config
self._alt_ota_url = getattr(config, "OTA_URL", "")
self.addCleanup(lambda: setattr(config, "OTA_URL", self._alt_ota_url))
self.config = config
def test_payload_wird_als_url_verwendet(self):
ausloesen = self.run_esp32.make_on_ota()
ausloesen("http://192.168.1.50:8000")
self.assertEqual(self.aufrufe, [("http://192.168.1.50:8000", None)])
def test_leere_payload_faellt_auf_config_ota_url_zurueck(self):
"""Genau wie set/mount_host: leer heisst "die Vorgabe aus config.py"."""
self.config.OTA_URL = "http://vorgabe:8000"
ausloesen = self.run_esp32.make_on_ota()
ausloesen("")
self.assertEqual(self.aufrufe, [("http://vorgabe:8000", None)])
def test_ohne_jede_url_wird_nichts_ausgeloest(self):
self.config.OTA_URL = ""
meldungen = []
ausloesen = self.run_esp32.make_on_ota(log=meldungen.append)
ausloesen("")
self.assertEqual(self.aufrufe, [])
self.assertTrue(any("keine URL" in m for m in meldungen))
def test_feed_wird_durchgereicht(self):
"""Ein Update ueber viele Dateien darf den Watchdog nicht ausloesen."""
feed = lambda: None
ausloesen = self.run_esp32.make_on_ota(feed=feed)
ausloesen("http://x")
self.assertIs(self.aufrufe[0][1], feed)
class TestLdrLeser(unittest.TestCase):
"""run_esp32.make_ldr_reader: ADC lesen, auf 12 bit bringen, spiegeln."""
@@ -557,8 +612,8 @@ class TestWifi(unittest.TestCase):
DEPLOYED = (
"ticks.py", "migra.py", "config.py", "settings.py", "transport.py",
"display.py", "coords.py", "lx200.py", "mount_client.py", "brightness.py",
"displayloop.py", "wifi.py", "mqtt.py", "run_esp32.py", "probe.py",
"main.py",
"displayloop.py", "wifi.py", "mqtt.py", "ota.py", "run_esp32.py",
"probe.py", "main.py",
)