"""Woher die Bruecke das Livebild holt. ./venv/bin/python -m unittest discover tests 🔑 Diese Datei gibt es wegen eines Ausfalls, der acht Tage lang im Betrieb stand, ohne dass irgendwo ein Fehler auftauchte: Seit dem Umzug nach Coolify (03.10.2026) trug die oeffentliche Instanz ``VOGELALBUM_LIVE_ERLAUBT=0`` -- richtig so, ein Bild aus dem eigenen Garten gehoert nicht nach draussen. Damit lieferte ``/api/zustand/`` aber ``bild_url: null``, und die Bruecke veroeffentlichte kein Livebild mehr. In Home Assistant blieb das letzte Bild stehen und **sah aus wie ein Livebild**. ⚠️ Der Riegel war gegen die Oeffentlichkeit gedacht und sperrte das eigene Haus mit aus -- obwohl die Bruecke im Heimnetz laeuft und den Verteiler direkt erreicht. Ein Schalter, zwei Zwecke. Gegengeprueft: Ohne ``live_adresse`` sind die Faelle "Vorgabe schlaegt API" und "Vorgabe trotz bild_url null" beide rot. """ from __future__ import annotations import os import unittest from homeassistant import bruecke class LiveAdresseTests(unittest.TestCase): SCHLUESSEL = ( "VOGELALBUM_LIVE_VOGELHAUS", "VOGELALBUM_LIVE_VOGELHAUS_2", ) def setUp(self): vorher = {k: os.environ.get(k) for k in self.SCHLUESSEL} for k in self.SCHLUESSEL: os.environ.pop(k, None) def zuruecksetzen(): for k, wert in vorher.items(): if wert is None: os.environ.pop(k, None) else: os.environ[k] = wert self.addCleanup(zuruecksetzen) @staticmethod def kamera(kennung="vogelhaus", bild_url=None): return {"kennung": kennung, "bild_url": bild_url} # --- ohne eigene Vorgabe: alles bleibt wie bisher ---------------- def test_ohne_alles_leer(self): self.assertEqual(bruecke.live_adresse(self.kamera()), "") def test_bild_url_aus_der_schnittstelle(self): kamera = self.kamera(bild_url="http://erkennung.local:8131/bild.jpg") self.assertEqual(bruecke.live_adresse(kamera), "http://erkennung.local:8131/bild.jpg") def test_stromadresse_wird_zum_einzelbild(self): """``/live`` ist der endlose MJPEG-Strom -- den darf sie nicht lesen.""" kamera = self.kamera(bild_url="http://erkennung.local:8131/live") self.assertEqual(bruecke.live_adresse(kamera), "http://erkennung.local:8131/bild.jpg") def test_schraegstrich_am_ende_stoert_nicht(self): kamera = self.kamera(bild_url="http://erkennung.local:8131/live/") self.assertEqual(bruecke.live_adresse(kamera), "http://erkennung.local:8131/bild.jpg") # --- mit eigener Vorgabe: der eigentliche Zweck ------------------ def test_vorgabe_auch_wenn_die_schnittstelle_nichts_liefert(self): """Der Ausfall vom 03.-11.10.2026: LIVE_ERLAUBT=0, also bild_url null.""" os.environ["VOGELALBUM_LIVE_VOGELHAUS"] = \ "http://erkennung.local:8131/bild.jpg" self.assertEqual(bruecke.live_adresse(self.kamera()), "http://erkennung.local:8131/bild.jpg") def test_vorgabe_schlaegt_die_schnittstelle(self): os.environ["VOGELALBUM_LIVE_VOGELHAUS"] = "http://127.0.0.1:8131/bild.jpg" kamera = self.kamera(bild_url="http://erkennung.local:8131/bild.jpg") self.assertEqual(bruecke.live_adresse(kamera), "http://127.0.0.1:8131/bild.jpg") def test_vorgabe_als_stromadresse(self): os.environ["VOGELALBUM_LIVE_VOGELHAUS"] = "http://erkennung.local:8131/live" self.assertEqual(bruecke.live_adresse(self.kamera()), "http://erkennung.local:8131/bild.jpg") # --- Randfaelle -------------------------------------------------- def test_kennung_mit_sonderzeichen(self): """"vogelhaus-2" wird zu VOGELALBUM_LIVE_VOGELHAUS_2.""" os.environ["VOGELALBUM_LIVE_VOGELHAUS_2"] = \ "http://erkennung.local:8133/bild.jpg" kamera = self.kamera(kennung="vogelhaus-2") self.assertEqual(bruecke.live_adresse(kamera), "http://erkennung.local:8133/bild.jpg") def test_ohne_kennung_keine_vorgabe(self): """Ohne Kennung gibt es keinen Variablennamen -- aber auch keinen Absturz.""" os.environ["VOGELALBUM_LIVE_VOGELHAUS"] = "http://127.0.0.1:8131/bild.jpg" kamera = {"bild_url": "http://erkennung.local:8131/bild.jpg"} self.assertEqual(bruecke.live_adresse(kamera), "http://erkennung.local:8131/bild.jpg") class EntwuerfeTests(unittest.TestCase): """Die Kamera-Entitaet darf nur entstehen, wenn es ein Bild dazu gibt. ⚠️ Sonst legt Home Assistant eine Kamera an, die nie etwas zeigt -- und eine leere Kachel ist schlimmer als keine. """ def setUp(self): self.vorher = os.environ.get("VOGELALBUM_LIVE_VOGELHAUS") os.environ.pop("VOGELALBUM_LIVE_VOGELHAUS", None) def zuruecksetzen(): if self.vorher is None: os.environ.pop("VOGELALBUM_LIVE_VOGELHAUS", None) else: os.environ["VOGELALBUM_LIVE_VOGELHAUS"] = self.vorher self.addCleanup(zuruecksetzen) @staticmethod def zustand(): return { "kameras": [{ "name": "Futterhaus Garten", "kennung": "vogelhaus", "aktiv": True, "online": True, "bild_url": None, }], "sammlung": {}, } def _hat_livebild(self): return any( inhalt.get("name") == "Livebild" for _, inhalt in bruecke.entwuerfe(self.zustand()) ) def test_ohne_adresse_keine_kamera_entitaet(self): self.assertFalse(self._hat_livebild()) def test_mit_vorgabe_entsteht_die_kamera_entitaet(self): os.environ["VOGELALBUM_LIVE_VOGELHAUS"] = \ "http://erkennung.local:8131/bild.jpg" self.assertTrue(self._hat_livebild()) if __name__ == "__main__": unittest.main()