merge: Panel und CLI der Station auf Englisch, IP-Zeile im Off-Bild
* station-english: feat(station): Panel und CLI auf Englisch, IP-Zeile im Off-Bild
This commit is contained in:
@@ -139,7 +139,7 @@ class Action:
|
||||
diesem Moment gar nicht vor Augen hat."""
|
||||
|
||||
def __init__(self, label: str, argv=(), kind: str = "cli", confirm: bool = True,
|
||||
done_text: str | None = None, confirm_title: str = "RUECKFRAGE",
|
||||
done_text: str | None = None, confirm_title: str = "CONFIRM",
|
||||
confirm_details=None) -> None:
|
||||
self.label = label
|
||||
self.argv = tuple(argv)
|
||||
@@ -154,16 +154,16 @@ class Action:
|
||||
|
||||
|
||||
def action_shutdown() -> Action:
|
||||
return Action("Herunterfahren", kind="shutdown", confirm=True,
|
||||
done_text="Faehrt herunter")
|
||||
return Action("Shut down", kind="shutdown", confirm=True,
|
||||
done_text="Shutting down")
|
||||
|
||||
|
||||
def action_migrate_listen() -> Action:
|
||||
"""Der naheliegende Weg in die Migration: Zugangspunkt an, aber es geht NICHTS raus,
|
||||
bis der Bediener eine erkannte Lampe freigibt ("arm"). Deshalb steht dieser Eintrag
|
||||
vor dem Automatikmodus und traegt die uebliche Rueckfrage."""
|
||||
return Action("Migration: horchen", ("migrate", "on", "--listen"),
|
||||
done_text="Migration: horcht")
|
||||
return Action("Migration: listen", ("migrate", "on", "--listen"),
|
||||
done_text="Migration: listening")
|
||||
|
||||
|
||||
def action_migrate_auto() -> Action:
|
||||
@@ -179,11 +179,11 @@ def action_migrate_auto() -> Action:
|
||||
danach nicht mehr mit seiner App spricht. Ein "Wirklich?" fragt nach etwas, das der
|
||||
Bediener in diesem Moment gar nicht vor Augen hat; deshalb steht die Folge in der
|
||||
Rueckfrage selbst."""
|
||||
return Action("Migration: AUTOMATIK", ("migrate", "on"),
|
||||
done_text="Migration: automatisch",
|
||||
confirm_title="ACHTUNG",
|
||||
confirm_details=["Flasht JEDE alte Klyqa-Lampe",
|
||||
"in Reichweite - auch fremde"])
|
||||
return Action("Migration: AUTOMATIC", ("migrate", "on"),
|
||||
done_text="Migration: automatic",
|
||||
confirm_title="WARNING",
|
||||
confirm_details=["Flashes EVERY old Klyqa lamp",
|
||||
"in range - other people's too"])
|
||||
|
||||
|
||||
def action_imageset(current: str) -> Action:
|
||||
@@ -196,19 +196,19 @@ def action_imageset(current: str) -> Action:
|
||||
Lampe, die sie erwischt."""
|
||||
target = "mit-wlan" if current != "mit-wlan" else "ohne-wlan"
|
||||
if target == "mit-wlan":
|
||||
return Action("Umschalten: mit WLAN", ("bildsatz", "mit-wlan"),
|
||||
done_text="Bildsatz: mit WLAN",
|
||||
confirm_title="ACHTUNG",
|
||||
confirm_details=["Jede migrierte Lampe bekommt",
|
||||
"dein WLAN-Passwort"])
|
||||
return Action("Umschalten: ohne WLAN", ("bildsatz", "ohne-wlan"),
|
||||
done_text="Bildsatz: ohne WLAN")
|
||||
return Action("Switch to: with Wi-Fi", ("bildsatz", "mit-wlan"),
|
||||
done_text="Image set: with Wi-Fi",
|
||||
confirm_title="WARNING",
|
||||
confirm_details=["Every migrated lamp receives",
|
||||
"your Wi-Fi password"])
|
||||
return Action("Switch to: no Wi-Fi", ("bildsatz", "ohne-wlan"),
|
||||
done_text="Image set: no Wi-Fi")
|
||||
|
||||
|
||||
def action_cancel() -> Action:
|
||||
"""Die Haltegeste in Ruhe: Station aus. Bewusst ohne Rueckfrage - sie IST die
|
||||
Rueckfrage (3 s halten), und sie ist der Notausgang, der ohne Umweg wirken muss."""
|
||||
return Action("Station aus", ("off",), confirm=False, done_text="Station aus")
|
||||
return Action("Station off", ("off",), confirm=False, done_text="Station off")
|
||||
|
||||
|
||||
def menu_actions(state: dict) -> list[Action]:
|
||||
@@ -221,7 +221,7 @@ def menu_actions(state: dict) -> list[Action]:
|
||||
|
||||
if not running:
|
||||
return [
|
||||
Action("Rettung starten", ("recovery", "on"), done_text="Rettung laeuft"),
|
||||
Action("Start recovery", ("recovery", "on"), done_text="Recovery running"),
|
||||
# Reihenfolge ist Absicht: der horchende Modus zuerst, weil er der
|
||||
# naheliegende und der harmlose ist - wer nur blaettert und irgendwo
|
||||
# loslaesst, landet nicht im Automatikmodus.
|
||||
@@ -237,13 +237,13 @@ def menu_actions(state: dict) -> list[Action]:
|
||||
if mode == "migration" and target:
|
||||
# Nur wenn last_seen wirklich ein Ziel kennt - ohne erkannte Lampe gibt es
|
||||
# nichts freizugeben, und "arm" ohne Ziel wuerde das CLI ohnehin abweisen.
|
||||
items.append(Action("Freigabe: {}".format(target), ("arm", str(target)),
|
||||
done_text="Freigegeben: {}".format(target)))
|
||||
items.append(Action("Station aus", ("off",), done_text="Station aus"))
|
||||
items.append(Action("Arm: {}".format(target), ("arm", str(target)),
|
||||
done_text="Armed: {}".format(target)))
|
||||
items.append(Action("Station off", ("off",), done_text="Station off"))
|
||||
items.append(action_imageset(imageset))
|
||||
# Veraendert nichts am Betrieb - einzige Auswahl ohne Rueckfrage.
|
||||
items.append(Action("Anzeige auffrischen", ("display", "test"), confirm=False,
|
||||
done_text="Anzeige aufgefrischt"))
|
||||
items.append(Action("Refresh display", ("display", "test"), confirm=False,
|
||||
done_text="Display refreshed"))
|
||||
items.append(action_shutdown())
|
||||
return items
|
||||
|
||||
@@ -561,32 +561,32 @@ class ButtonUI:
|
||||
and self.screen in ("idle", "busy")):
|
||||
held = now - self._press_t
|
||||
if self._hold_stage == 1:
|
||||
return _view("hold", "STATION AUS",
|
||||
"Herunterfahren in {} s".format(
|
||||
return _view("hold", "STATION OFF",
|
||||
"Shutting down in {} s".format(
|
||||
_countdown(SHUTDOWN_HOLD_S, held)),
|
||||
["Weiter halten - Loslassen bricht ab"])
|
||||
["Keep holding - release to cancel"])
|
||||
if held >= SHORT_PRESS_MAX_S:
|
||||
return _view("hold", "HALTEN",
|
||||
"Abbruch in {} s".format(_countdown(CANCEL_HOLD_S, held)),
|
||||
["Weiter halten - Loslassen bricht ab"])
|
||||
return _view("hold", "HOLD",
|
||||
"Cancel in {} s".format(_countdown(CANCEL_HOLD_S, held)),
|
||||
["Keep holding - release to cancel"])
|
||||
if self.screen == "menu" and self.items:
|
||||
selected = self.items[self.index]
|
||||
rest = self.items[self.index + 1:] + self.items[:self.index]
|
||||
return _view("menu", "MENUE", "> " + selected.label,
|
||||
[a.label for a in rest] + ["kurz: weiter, halten: waehlen"])
|
||||
return _view("menu", "MENU", "> " + selected.label,
|
||||
[a.label for a in rest] + ["tap: next, hold: select"])
|
||||
if self.screen == "confirm" and self.pending is not None:
|
||||
# Die Bedienhinweise stehen IMMER und IMMER ZULETZT - eine Rueckfrage, bei
|
||||
# der man raten muss, wie man ablehnt, ist keine. Eine eigene Warnung des
|
||||
# Eintrags (confirm_details) kommt davor, damit sie zuerst gelesen wird.
|
||||
details = list(self.pending.confirm_details or [])
|
||||
details += ["halten (1,5 s): ja", "kurz: nein"]
|
||||
details += ["hold (1.5 s): yes", "tap: no"]
|
||||
return _view("confirm", self.pending.confirm_title, self.pending.label,
|
||||
details)
|
||||
if self.screen == "busy" and self.busy is not None:
|
||||
return _view("busy", "BITTE WARTEN", self.busy.label, [])
|
||||
return _view("busy", "PLEASE WAIT", self.busy.label, [])
|
||||
if self.result is not None:
|
||||
text, ok = self.result
|
||||
return _view("result", "FERTIG" if ok else "FEHLER", text, [])
|
||||
return _view("result", "DONE" if ok else "ERROR", text, [])
|
||||
return {"view": "idle", "active": False}
|
||||
|
||||
|
||||
|
||||
@@ -237,6 +237,35 @@ def _read_imageset(imageset_path: Path, conf: dict[str, str]) -> tuple[str, bool
|
||||
return name, False
|
||||
|
||||
|
||||
def local_ip() -> str:
|
||||
"""Die Adresse, unter der der Pi im Heimnetz erreichbar ist, oder "" wenn keine da ist.
|
||||
|
||||
Ueber einen UDP-Socket, der nur verbunden und nie benutzt wird: das erzeugt keinen
|
||||
Datenverkehr und fragt kein DNS, liefert aber genau die Adresse, die der Kernel fuer
|
||||
die Standardroute waehlen wuerde - also die, ueber die der Bediener tatsaechlich per
|
||||
SSH hereinkommt. Ein Aufruf von "hostname -I" waere die naheliegende Alternative,
|
||||
listet aber ALLE Adressen (inklusive der des Zugangspunkts und IPv6) und braucht einen
|
||||
Unterprozess in einer Schleife, die alle 5 s laeuft.
|
||||
|
||||
Wirft nie: ohne Netz gibt es keine Route, dann liefert der Aufruf "" und die Anzeige
|
||||
sagt das - eine Ausnahme aus der Adressermittlung darf das Panel nicht aufhalten."""
|
||||
import socket
|
||||
|
||||
sock = None
|
||||
try:
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
# Die Adresse wird nie angesprochen; sie waehlt nur die Route aus. Bewusst eine
|
||||
# dokumentierte Testadresse (RFC 5737) statt eines echten Ziels wie 8.8.8.8 -
|
||||
# es soll auch dann nichts Fremdes anklingen, wenn jemand mitliest.
|
||||
sock.connect(("192.0.2.1", 9))
|
||||
return str(sock.getsockname()[0])
|
||||
except OSError:
|
||||
return ""
|
||||
finally:
|
||||
if sock is not None:
|
||||
sock.close()
|
||||
|
||||
|
||||
def build_state(run_dir: Path = RUN_DIR, conf_path: Path = CONF_PATH,
|
||||
service_name: str = SERVICE_NAME,
|
||||
imageset_path: Path = IMAGESET_FILE) -> dict:
|
||||
@@ -262,7 +291,13 @@ def build_state(run_dir: Path = RUN_DIR, conf_path: Path = CONF_PATH,
|
||||
imageset, imageset_ok = _read_imageset(imageset_path, conf)
|
||||
|
||||
if not _service_active(service_name):
|
||||
return {"mode": "off", "imageset": imageset, "imageset_ok": imageset_ok}
|
||||
# Die IP-Zeile gibt es NUR hier: ist die Station aus, ist der Pi ein gewoehnlicher
|
||||
# Rechner im Heimnetz, und die einzige Auskunft, die man dann am Panel braucht,
|
||||
# ist die Adresse, unter der man per SSH drankommt. Im Betrieb waere sie sinnlos
|
||||
# (wlan0 traegt dann 192.168.8.1, das steht ohnehin auf jedem Statusbild) und
|
||||
# wuerde den knappen Platz fuer Betriebsauskuenfte wegnehmen.
|
||||
return {"mode": "off", "imageset": imageset, "imageset_ok": imageset_ok,
|
||||
"ip": local_ip()}
|
||||
|
||||
mode = _read_mode_file(run_dir)
|
||||
status = _read_json(run_dir / "status.json")
|
||||
|
||||
@@ -172,7 +172,7 @@ FONT_SEARCH_PATHS = (
|
||||
("/Library/Fonts/Arial.ttf", "/Library/Fonts/Arial Bold.ttf"),
|
||||
)
|
||||
|
||||
MODE_LABELS = {"recovery": "RETTUNG", "migration": "MIGRATION"}
|
||||
MODE_LABELS = {"recovery": "RECOVERY", "migration": "MIGRATION"}
|
||||
# "armed" wird gesondert behandelt (es zieht zusaetzlich eine Beiwerkzeile mit dem
|
||||
# Zielnamen nach sich), deshalb hier nicht gelistet.
|
||||
PHASE_LABELS = {"auto": "AUTO", "listen": "LISTEN"}
|
||||
@@ -186,8 +186,8 @@ PHASE_LABELS = {"auto": "AUTO", "listen": "LISTEN"}
|
||||
# breiteren Schrift hoechstens am Ende beschnitten wird, ohne dass dabei eine Auskunft
|
||||
# verloren geht - der Zustand "freigegeben" ist der einzige mit diesem Label) und der
|
||||
# Zielname als eigene Beiwerkzeile ueber die volle Panelbreite.
|
||||
ARMED_LABEL = "FREIGEGEBEN"
|
||||
ARMED_DETAIL_PREFIX = "Freigabe: "
|
||||
ARMED_LABEL = "ARMED"
|
||||
ARMED_DETAIL_PREFIX = "Armed: "
|
||||
|
||||
# Der Bildsatz-Umschalter in der Fusszeile. Bewusst als Aussage ueber die LAMPE formuliert
|
||||
# und nicht ueber das Verzeichnis: "migrate-wlan" sagt dem Bediener am Geraet nichts,
|
||||
@@ -201,16 +201,16 @@ ARMED_DETAIL_PREFIX = "Freigabe: "
|
||||
# mit 246,0 von 246 px haarscharf; deshalb hat es niemand bemerkt. Breitenbudget siehe
|
||||
# test_imageset_footer_fits_the_panel_with_room_for_a_wider_font.
|
||||
IMAGESET_LABELS = {
|
||||
"ohne-wlan": "ohne WLAN-Daten (Onboarding-AP)",
|
||||
"mit-wlan": "MIT WLAN-Daten (eigenes Netz)",
|
||||
"ohne-wlan": "no Wi-Fi data (onboarding AP)",
|
||||
"mit-wlan": "WITH Wi-Fi data (own network)",
|
||||
}
|
||||
# Fehlt das Verzeichnis des gewaehlten Satzes, ERSETZT die Warnung die Zeile, statt sie
|
||||
# zu verlaengern. Als angehaengtes Suffix (" - SATZ FEHLT", bis 2026-09-15) war sie in
|
||||
# BEIDEN Richtungen und in beiden Schriften ausserhalb der Panelbreite - die einzige
|
||||
# Zeile, die sagt, dass die Station gar nichts ausliefern kann, war damit nie zu sehen.
|
||||
IMAGESET_MISSING_LABELS = {
|
||||
"ohne-wlan": "SATZ FEHLT: ohne WLAN-Daten",
|
||||
"mit-wlan": "SATZ FEHLT: MIT WLAN-Daten",
|
||||
"ohne-wlan": "SET MISSING: no Wi-Fi data",
|
||||
"mit-wlan": "SET MISSING: WITH Wi-Fi data",
|
||||
}
|
||||
|
||||
# Der Abschlussbildschirm: was auf dem Panel stehen bleibt, wenn der Pi heruntergefaehrt
|
||||
@@ -228,10 +228,10 @@ FINAL_LABELS = {
|
||||
# Geraet als "Strom kann getrennt wer..." an - siehe die Fusszeile weiter oben, es
|
||||
# ist derselbe Fehler. Alle festen Beschriftungen haengen jetzt an
|
||||
# test_fixed_labels_fit_the_panel_with_room_for_a_wider_font.
|
||||
"poweroff": ("AUSGESCHALTET", "Strom trennen erlaubt", []),
|
||||
"reboot": ("NEUSTART", "Strom NICHT trennen!",
|
||||
["Der Pi kommt in rund 2 min wieder.",
|
||||
"Solange bleibt dieses Bild stehen."]),
|
||||
"poweroff": ("POWERED OFF", "Safe to unplug", []),
|
||||
"reboot": ("REBOOTING", "Do NOT unplug!",
|
||||
["The Pi is back in about 2 min.",
|
||||
"This screen stays until then."]),
|
||||
}
|
||||
|
||||
# Woraus die Station die Signiervariante abgeleitet hat (products.classify_variant):
|
||||
@@ -293,7 +293,7 @@ def blocks(state: dict) -> list[tuple[str, str]]:
|
||||
if label is None:
|
||||
# Unbekannte Spielart - lieber ehrlich nichts behaupten als "ausgeschaltet"
|
||||
# zu sagen, wenn gleich wieder gebootet wird.
|
||||
return [("Faehrt herunter", ROLE_BIG)]
|
||||
return [("Shutting down", ROLE_BIG)]
|
||||
title, hero, details = label
|
||||
return ([(title, ROLE_HEADER), (hero, ROLE_HERO)]
|
||||
+ [(d, ROLE_DETAIL) for d in details])
|
||||
@@ -308,14 +308,24 @@ def blocks(state: dict) -> list[tuple[str, str]]:
|
||||
# diesen Modus selbst, server.py kennt ihn nicht. Bewusst keine weiteren Zeilen
|
||||
# (insbesondere keine SSID - die waere aus einer alten Konfigurationsdatei und
|
||||
# gerade nicht aktiv). Genau deshalb ROLE_BIG: die Zeile ist der ganze Inhalt.
|
||||
out.append(("Station aus", ROLE_BIG))
|
||||
out.append(("Station off", ROLE_BIG))
|
||||
# Die Adresse, unter der der Pi gerade erreichbar ist - die einzige Auskunft, die
|
||||
# man bei ausgeschalteter Station am Geraet wirklich braucht (per SSH draufgehen,
|
||||
# ohne den Router zu befragen). display.py legt sie nur in DIESEN Zustand; fehlt
|
||||
# das Feld (aeltere display.py-Fassung, von Hand gebauter Zustand im Test), bleibt
|
||||
# die Zeile weg statt eine Adresse zu erfinden. Ist das Feld da, aber leer, sagt
|
||||
# die Zeile ausdruecklich, dass kein Netz da ist: "keine Zeile" und "kein Netz"
|
||||
# sind zwei verschiedene Auskuenfte, und die zweite erklaert, warum SSH scheitert.
|
||||
ip = state.get("ip")
|
||||
if isinstance(ip, str):
|
||||
out.append((f"IP {ip}" if ip else "no network", ROLE_DETAIL))
|
||||
return out + footer
|
||||
|
||||
if mode not in ("recovery", "migration"):
|
||||
# Unbekannter oder fehlender Modus, z.B. ein frisch aufgesetzter Pi, auf dem noch
|
||||
# nichts geschrieben wurde. Lieber ehrlich "unbekannt" zeigen als einen Modus zu
|
||||
# unterstellen.
|
||||
out.append((f"Status unbekannt ({mode})" if mode else "Status unbekannt", ROLE_BIG))
|
||||
out.append((f"Status unknown ({mode})" if mode else "Status unknown", ROLE_BIG))
|
||||
return out + footer
|
||||
|
||||
out.append((MODE_LABELS[mode], ROLE_HEADER))
|
||||
@@ -409,12 +419,12 @@ def blocks(state: dict) -> list[tuple[str, str]]:
|
||||
autooff_minutes = state.get("autooff_minutes")
|
||||
if isinstance(autooff_minutes, int) and not isinstance(autooff_minutes, bool):
|
||||
if autooff_minutes > 0:
|
||||
out.append(("Auto-Aus in {} min".format(autooff_minutes), ROLE_DETAIL))
|
||||
out.append(("Auto-off in {} min".format(autooff_minutes), ROLE_DETAIL))
|
||||
else:
|
||||
out.append(("Auto-Aus steht an", ROLE_DETAIL))
|
||||
out.append(("Auto-off pending", ROLE_DETAIL))
|
||||
|
||||
served = state.get("served")
|
||||
out.append((f"Ausgeliefert: {served if isinstance(served, int) else 0}", ROLE_DETAIL))
|
||||
out.append((f"Served: {served if isinstance(served, int) else 0}", ROLE_DETAIL))
|
||||
|
||||
recent = state.get("recent")
|
||||
if isinstance(recent, list) and recent:
|
||||
@@ -467,7 +477,7 @@ def _ui_blocks(ui: dict) -> list[tuple[str, str]]:
|
||||
# Es laeuft eine Bedienung, aber die Datei traegt keinen brauchbaren Text
|
||||
# (abgeschnitten, fremde Fassung). Lieber gross sagen, dass bedient wird, als
|
||||
# ein leeres Panel zu zeigen, das nach "haengt" aussieht.
|
||||
out.append(("Bedienung", ROLE_BIG))
|
||||
out.append(("Button menu", ROLE_BIG))
|
||||
return out
|
||||
|
||||
|
||||
|
||||
+110
-110
@@ -107,14 +107,14 @@ elif [ -e "$CONF" ]; then
|
||||
# Rechte aufzuweichen.
|
||||
eval "$(sudo cat "$CONF")"
|
||||
else
|
||||
echo "Fehler: $CONF fehlt - zuerst tools/station/setup_pi.sh ausfuehren" >&2
|
||||
echo "Error: $CONF missing - run tools/station/setup_pi.sh first" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
for _key in SSID PSK IMAGE_DIR HOME_CONNECTION AP_CONNECTION \
|
||||
MIGRATE_AP_CONNECTION MIGRATE_SSID MIGRATE_PSK EPD_REVISION; do
|
||||
eval "test -n \"\${$_key-}\"" || {
|
||||
echo "Fehler: $CONF: Schluessel $_key fehlt oder ist leer - tools/station/setup_pi.sh ausfuehren" >&2
|
||||
echo "Error: $CONF: key $_key missing or empty - run tools/station/setup_pi.sh" >&2
|
||||
exit 1
|
||||
}
|
||||
done
|
||||
@@ -125,7 +125,7 @@ need_profile() {
|
||||
# -F: der Profilname ist ein Konfigurationswert, kein BRE-Muster - ein Name mit
|
||||
# "." oder "[" wuerde mit "grep -x" sonst falsch (oder gar nicht) treffen.
|
||||
nmcli -t --escape no -f NAME con show | grep -Fqx "$1" || {
|
||||
echo "Fehler: NetworkManager-Profil '$1' fehlt - tools/station/setup_pi.sh ausfuehren" >&2
|
||||
echo "Error: NetworkManager profile '$1' missing - run tools/station/setup_pi.sh" >&2
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
@@ -163,8 +163,8 @@ imageset_dir() {
|
||||
|
||||
imageset_label() {
|
||||
case "$1" in
|
||||
mit-wlan) printf 'mit-wlan (Lampe haengt sofort im eigenen WLAN)' ;;
|
||||
*) printf 'ohne-wlan (Lampe startet im Onboarding-Zugangspunkt)' ;;
|
||||
mit-wlan) printf 'mit-wlan (lamp joins your Wi-Fi right away)' ;;
|
||||
*) printf 'ohne-wlan (lamp starts its onboarding access point)' ;;
|
||||
esac
|
||||
}
|
||||
|
||||
@@ -194,25 +194,25 @@ bildsatz_cmd() {
|
||||
case "$_want" in
|
||||
ohne-wlan|mit-wlan) ;;
|
||||
*)
|
||||
echo "Fehler: unbekannter Bildsatz '$_want' - erlaubt sind 'ohne-wlan' und 'mit-wlan'" >&2
|
||||
echo "Error: unknown image set '$_want' - allowed are 'ohne-wlan' and 'mit-wlan'" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
imageset_set "$_want" || {
|
||||
echo "Fehler: $IMAGESET_FILE konnte nicht geschrieben werden" >&2
|
||||
echo "Error: could not write $IMAGESET_FILE" >&2
|
||||
exit 1
|
||||
}
|
||||
echo "Bildsatz: $(imageset_label "$_want")"
|
||||
[ "$_now" = "$_want" ] && echo " (war schon eingestellt)"
|
||||
echo "Image set: $(imageset_label "$_want")"
|
||||
[ "$_now" = "$_want" ] && echo " (was already set)"
|
||||
_dir=$(imageset_dir "$_want")
|
||||
if [ "$(find "$_dir" -maxdepth 1 -name '*.img' 2>/dev/null | wc -l | tr -d ' ')" -eq 0 ]; then
|
||||
# Kein stiller Rueckfall auf den anderen Satz - server.py liefert dann gar nichts
|
||||
# aus (Status "noimageset"), und das Panel sagt "FEHLT". Hier trotzdem sofort
|
||||
# aus (Status "noimageset"), und das Panel sagt "MISSING". Hier trotzdem sofort
|
||||
# warnen, damit niemand erst an der Lampe merkt, dass nichts kommt.
|
||||
echo "Warnung: $_dir fehlt oder ist leer - in diesem Bildsatz wird NICHTS ausgeliefert. tools/station/deploy.sh erneut laufen lassen." >&2
|
||||
echo "Warning: $_dir missing or empty - this image set serves NOTHING. Run tools/station/deploy.sh again." >&2
|
||||
fi
|
||||
if [ "$_want" = "mit-wlan" ]; then
|
||||
echo "Achtung: migrierte Lampen haengen damit sofort im WLAN aus config.local.cmake - auch fremde Lampen, die im Automatikmodus mitgenommen werden." >&2
|
||||
echo "Caution: migrated lamps join the Wi-Fi from config.local.cmake right away - including lamps that are not yours and got picked up in automatic mode." >&2
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -225,18 +225,18 @@ imageset_status_lines() {
|
||||
# Verzeichnis liefert genauso nichts aus wie ein fehlendes.
|
||||
_n=$(migrate_images | wc -l | tr -d ' ')
|
||||
if [ "$_n" -gt 0 ]; then
|
||||
_state="$_n Image(s)"
|
||||
_state="$_n image(s)"
|
||||
else
|
||||
_state="FEHLT - es wird nichts ausgeliefert"
|
||||
_state="MISSING - nothing will be served"
|
||||
fi
|
||||
printf 'Bildsatz: %s\n' "$(imageset_label "$_set")"
|
||||
printf 'Image set: %s\n' "$(imageset_label "$_set")"
|
||||
printf ' %s (%s)\n' "$_dir" "$_state"
|
||||
# Steht in der Datei etwas, das kein Bildsatzname ist, gilt stillschweigend die
|
||||
# Vorgabe - das ist richtig so (sicher), aber der Bediener muss es erfahren: sonst
|
||||
# glaubt er, sein Eintrag von Hand haette gewirkt.
|
||||
_raw=$(imageset_raw)
|
||||
if [ -n "$_raw" ] && [ "$_raw" != "$_set" ]; then
|
||||
printf " (unbekannte Einstellung '%s' in %s - Vorgabe gilt)\n" \
|
||||
printf " (unknown setting '%s' in %s - default applies)\n" \
|
||||
"$_raw" "$IMAGESET_FILE"
|
||||
fi
|
||||
}
|
||||
@@ -276,7 +276,7 @@ ap_switch() {
|
||||
# gehoert zum Wert).
|
||||
_active=$(nmcli -t --escape no -f GENERAL.CONNECTION device show wlan0 2>/dev/null | cut -d: -f2-)
|
||||
if [ "$_active" = "$AP_CONNECTION" ] || [ "$_active" = "$MIGRATE_AP_CONNECTION" ]; then
|
||||
echo "Fehler: wlan0 zeigt nach dem Ausschalten immer noch '$_active' aktiv - NetworkManager pruefen (Zugangspunkt bleibt an)" >&2
|
||||
echo "Error: after switching off, wlan0 still shows '$_active' active - check NetworkManager (access point stays up)" >&2
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
@@ -287,7 +287,7 @@ ap_switch() {
|
||||
if [ "$_active" != "$_target" ]; then
|
||||
sudo nmcli con down "$AP_CONNECTION" >/dev/null 2>&1 || true
|
||||
sudo nmcli con down "$MIGRATE_AP_CONNECTION" >/dev/null 2>&1 || true
|
||||
echo "Fehler: wlan0 zeigt nach dem Umschalten auf '$_target' unerwartet '$_active' - beide Stationsprofile wieder heruntergefahren" >&2
|
||||
echo "Error: after switching to '$_target', wlan0 unexpectedly shows '$_active' - both station profiles brought down again" >&2
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
@@ -326,16 +326,16 @@ write_control() {
|
||||
autooff_check_minutes() {
|
||||
case "${1:-}" in
|
||||
'' )
|
||||
echo "Fehler: '--for' braucht eine Angabe in Minuten ($AUTOOFF_MIN_MINUTES-$AUTOOFF_MAX_MINUTES)" >&2
|
||||
echo "Error: '--for' needs a value in minutes ($AUTOOFF_MIN_MINUTES-$AUTOOFF_MAX_MINUTES)" >&2
|
||||
return 1
|
||||
;;
|
||||
*[!0-9]* )
|
||||
echo "Fehler: '$1' ist keine Minutenangabe - erlaubt sind ganze Zahlen von $AUTOOFF_MIN_MINUTES bis $AUTOOFF_MAX_MINUTES" >&2
|
||||
echo "Error: '$1' is not a number of minutes - allowed are whole numbers from $AUTOOFF_MIN_MINUTES to $AUTOOFF_MAX_MINUTES" >&2
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
if [ "$1" -lt "$AUTOOFF_MIN_MINUTES" ] || [ "$1" -gt "$AUTOOFF_MAX_MINUTES" ]; then
|
||||
echo "Fehler: $1 Minuten liegen ausserhalb von $AUTOOFF_MIN_MINUTES-$AUTOOFF_MAX_MINUTES" >&2
|
||||
echo "Error: $1 minutes is outside $AUTOOFF_MIN_MINUTES-$AUTOOFF_MAX_MINUTES" >&2
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
@@ -424,11 +424,11 @@ autooff_arm() {
|
||||
set -- "$@" "--setenv=NINLIGHT_STATION_LIB=$NINLIGHT_STATION_LIB"
|
||||
fi
|
||||
if ! sudo systemd-run "$@" "$SELF" __autooff >/dev/null; then
|
||||
echo "Fehler: Zeitschalter liess sich nicht anlegen (systemd-run '$AUTOOFF_UNIT') - die Station bleibt AN, 'ninlight-station off' nicht vergessen." >&2
|
||||
echo "Error: could not create the timer (systemd-run '$AUTOOFF_UNIT') - the station stays ON, do not forget 'ninlight-station off'." >&2
|
||||
return 1
|
||||
fi
|
||||
autooff_write "$_req" "$(( $(date +%s) + _delay ))" "$_deferrals" || {
|
||||
echo "Fehler: $AUTOOFF_FILE konnte nicht geschrieben werden - Zeitschalter wieder abgeraeumt" >&2
|
||||
echo "Error: could not write $AUTOOFF_FILE - timer removed again" >&2
|
||||
autooff_clear
|
||||
return 1
|
||||
}
|
||||
@@ -455,7 +455,7 @@ autooff_busy() {
|
||||
# "ja, beschaeftigt", haette ein fehlendes ss den Zeitschalter bis zur
|
||||
# Obergrenze ausgehebelt - und der Zeitschalter ist angefordert worden, WEIL der
|
||||
# Nutzer sonst nicht mehr an den Pi kommt.
|
||||
echo "Warnung: 'ss' nicht gefunden - laufende Auslieferungen konnten nicht geprueft werden" >&2
|
||||
echo "Warning: 'ss' not found - could not check for downloads in progress" >&2
|
||||
return 1
|
||||
fi
|
||||
_conns=$("$_ss" -Htn state established "( sport = :80 )" dst "$AUTOOFF_STATION_NET" 2>/dev/null | grep -c . || true)
|
||||
@@ -485,10 +485,10 @@ autooff_fire() {
|
||||
if [ "$_deferrals" -lt "$AUTOOFF_MAX_DEFERRALS" ]; then
|
||||
_deferrals=$((_deferrals + 1))
|
||||
autooff_write "$_req" "$(( $(date +%s) + AUTOOFF_RETRY_S ))" "$_deferrals"
|
||||
echo "Zeitschalter verschoben ($_deferrals/$AUTOOFF_MAX_DEFERRALS): eine Auslieferung laeuft noch"
|
||||
echo "Timer deferred ($_deferrals/$AUTOOFF_MAX_DEFERRALS): a download is still running"
|
||||
exit 0
|
||||
fi
|
||||
echo "Warnung: $AUTOOFF_MAX_DEFERRALS Verschiebungen erreicht - es steht immer noch eine Verbindung offen, sie haengt vermutlich. Station wird jetzt trotzdem abgeschaltet." >&2
|
||||
echo "Warning: reached $AUTOOFF_MAX_DEFERRALS deferrals - a connection is still open and has probably stalled. Switching the station off anyway." >&2
|
||||
fi
|
||||
full_off
|
||||
}
|
||||
@@ -529,7 +529,7 @@ start_and_verify() {
|
||||
# Lebendigkeitspruefung unten je erreicht wird, und der eben
|
||||
# hochgefahrene Zugangspunkt bliebe offen ohne Server dahinter stehen.
|
||||
sudo nmcli con down "$_ap" >/dev/null 2>&1 || true
|
||||
echo "Fehler: ninlight-station liess sich nicht (neu) starten - Zugangspunkt wieder abgeschaltet. Details: journalctl -u ninlight-station" >&2
|
||||
echo "Error: could not (re)start ninlight-station - access point switched off again. Details: journalctl -u ninlight-station" >&2
|
||||
exit 1
|
||||
fi
|
||||
# "systemctl restart" meldet bei Type=simple schon Erfolg, sobald der Prozess
|
||||
@@ -542,14 +542,14 @@ start_and_verify() {
|
||||
# sonst suggeriert die Meldung unten einen saubereren Zustand als er ist.
|
||||
sudo systemctl stop ninlight-station 2>/dev/null || true
|
||||
sudo nmcli con down "$_ap" >/dev/null 2>&1 || true
|
||||
echo "Fehler: ninlight-station ist nach dem Start nicht aktiv - Zugangspunkt wieder abgeschaltet. Details: journalctl -u ninlight-station" >&2
|
||||
echo "Error: ninlight-station is not active after starting - access point switched off again. Details: journalctl -u ninlight-station" >&2
|
||||
exit 1
|
||||
fi
|
||||
_actual_mode=$(cat "$RUN_DIR/mode" 2>/dev/null || true)
|
||||
if [ "$_actual_mode" != "$_expect_mode" ]; then
|
||||
sudo systemctl stop ninlight-station 2>/dev/null || true
|
||||
sudo nmcli con down "$_ap" >/dev/null 2>&1 || true
|
||||
echo "Fehler: $RUN_DIR/mode enthaelt nach dem Neustart '$_actual_mode' statt '$_expect_mode' - RuntimeDirectory wurde vermutlich geleert (RuntimeDirectoryPreserve=restart in der Unit pruefen). Zugangspunkt wieder abgeschaltet." >&2
|
||||
echo "Error: after the restart $RUN_DIR/mode holds '$_actual_mode' instead of '$_expect_mode' - the RuntimeDirectory was probably wiped (check RuntimeDirectoryPreserve=restart in the unit). Access point switched off again." >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ -n "$_expect_phase" ]; then
|
||||
@@ -568,7 +568,7 @@ PY
|
||||
if [ "$_actual_phase" != "$_expect_phase" ]; then
|
||||
sudo systemctl stop ninlight-station 2>/dev/null || true
|
||||
sudo nmcli con down "$_ap" >/dev/null 2>&1 || true
|
||||
echo "Fehler: $RUN_DIR/control.json enthaelt nach dem Neustart Phase '$_actual_phase' statt '$_expect_phase' - RuntimeDirectory wurde vermutlich geleert. Zugangspunkt wieder abgeschaltet." >&2
|
||||
echo "Error: after the restart $RUN_DIR/control.json holds phase '$_actual_phase' instead of '$_expect_phase' - the RuntimeDirectory was probably wiped. Access point switched off again." >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
@@ -606,26 +606,26 @@ parse_for_option() {
|
||||
autooff_arm_if_requested() {
|
||||
[ -n "$_for_minutes" ] || return 0
|
||||
autooff_arm "$_for_minutes" "$((_for_minutes * 60))" 0 || exit 1
|
||||
echo "Zeitschalter: Auto-Aus in $_for_minutes min ('ninlight-station renew' verlaengert, 'hold' bestellt ab)"
|
||||
echo "Timer: auto-off in $_for_minutes min ('ninlight-station renew' extends it, 'hold' cancels it)"
|
||||
}
|
||||
|
||||
recovery_on() {
|
||||
parse_for_option recovery "$@"
|
||||
need_profile "$AP_CONNECTION"
|
||||
n=$(images | wc -l | tr -d ' ')
|
||||
[ "$n" -gt 0 ] || echo "Warnung: $IMAGE_DIR enthaelt kein Image - die Station antwortet auf alles mit 404" >&2
|
||||
[ "$n" -gt 0 ] || echo "Warning: $IMAGE_DIR holds no image - the station answers everything with 404" >&2
|
||||
ap_switch "$AP_CONNECTION"
|
||||
write_mode "recovery"
|
||||
start_and_verify "$AP_CONNECTION" "recovery"
|
||||
echo "Station an: SSID $SSID auf 192.168.8.1, $n Image(s)"
|
||||
echo "Station on: SSID $SSID at 192.168.8.1, $n image(s)"
|
||||
# Mit dem oeffentlichen Default-Passwort (im Repo dokumentiert) kann jeder in
|
||||
# Funkreichweite, der es kennt, jeder Lampe beliebige Firmware unterschieben - das ist
|
||||
# die eine Haelfte der Betriebsregeln aus README.md/§9, die andere (Station wieder
|
||||
# ausschalten) steht unten.
|
||||
if [ "$PSK" = "ninlight-recovery" ]; then
|
||||
echo "Warnung: PSK ist noch der oeffentliche Default 'ninlight-recovery' - eigenes Passwort in /etc/ninlight-station.conf setzen." >&2
|
||||
echo "Warning: PSK is still the public default 'ninlight-recovery' - set your own password in /etc/ninlight-station.conf." >&2
|
||||
fi
|
||||
echo "Nicht vergessen: 'ninlight-station off', wenn du fertig bist."
|
||||
echo "Do not forget: 'ninlight-station off' when you are done."
|
||||
autooff_arm_if_requested
|
||||
}
|
||||
|
||||
@@ -658,23 +658,23 @@ migrate_on() {
|
||||
_set=$(imageset_get)
|
||||
_setdir=$(imageset_dir "$_set")
|
||||
n=$(migrate_images | wc -l | tr -d ' ')
|
||||
[ "$n" -gt 0 ] || echo "Warnung: $_setdir enthaelt keine Migrationsimages - die Station antwortet auf alles mit 404" >&2
|
||||
[ "$n" -gt 0 ] || echo "Warning: $_setdir holds no migration images - the station answers everything with 404" >&2
|
||||
# Faehrt ninlight-recovery (falls es laeuft) herunter, BEVOR qconnex-firmware
|
||||
# hochkommt, und prueft danach nach, dass wirklich nur eines aktiv ist.
|
||||
ap_switch "$MIGRATE_AP_CONNECTION"
|
||||
write_mode "migration"
|
||||
write_control "$_phase" "" || {
|
||||
echo "Fehler: $RUN_DIR/control.json konnte nicht geschrieben werden" >&2
|
||||
echo "Error: could not write $RUN_DIR/control.json" >&2
|
||||
exit 1
|
||||
}
|
||||
start_and_verify "$MIGRATE_AP_CONNECTION" "migration" "$_phase"
|
||||
echo "Migration an: SSID $MIGRATE_SSID auf 192.168.8.1, Phase $_phase, $n Migrationsimage(s)"
|
||||
echo "Migration on: SSID $MIGRATE_SSID at 192.168.8.1, phase $_phase, $n migration image(s)"
|
||||
# Der Bildsatz gehoert in JEDE Einschaltmeldung: er entscheidet, ob die migrierte
|
||||
# Lampe das WLAN-Passwort des Bedieners mitbekommt, und er kann von vorletzter Woche
|
||||
# stehen.
|
||||
echo "Bildsatz: $(imageset_label "$_set")"
|
||||
echo "Achtung: das Passwort dieses Zugangspunkts ist aus der alten Firmware oeffentlich bekannt - siehe README.md." >&2
|
||||
echo "Nicht vergessen: 'ninlight-station off', wenn du fertig bist."
|
||||
echo "Image set: $(imageset_label "$_set")"
|
||||
echo "Caution: the password of this access point is publicly known from the old firmware - see README.md." >&2
|
||||
echo "Do not forget: 'ninlight-station off' when you are done."
|
||||
autooff_arm_if_requested
|
||||
}
|
||||
|
||||
@@ -722,7 +722,7 @@ display_run() {
|
||||
display_service_resume() {
|
||||
[ "${_was_active:-0}" -eq 1 ] || return 0
|
||||
sudo systemctl start ninlight-display 2>/dev/null \
|
||||
|| echo "Warnung: Anzeigedienst konnte nicht wieder gestartet werden - 'systemctl status ninlight-display' pruefen" >&2
|
||||
|| echo "Warning: could not start the display service again - check 'systemctl status ninlight-display'" >&2
|
||||
}
|
||||
|
||||
display_handover() {
|
||||
@@ -789,7 +789,7 @@ display_off() {
|
||||
sudo systemctl stop ninlight-display 2>/dev/null || true
|
||||
# Kein Abbruch, wenn das Panel fehlt: display.py beendet sich dann selbst mit 0, und
|
||||
# ein Fehler an der Anzeige darf das Ausschalten der Station nie aufhalten.
|
||||
display_run --clear || echo "Warnung: Panel konnte nicht geleert werden - Anzeige pruefen" >&2
|
||||
display_run --clear || echo "Warning: could not clear the panel - check the display" >&2
|
||||
}
|
||||
|
||||
# --- Abschlussbildschirm beim Herunterfahren und beim Neustart ----------------------
|
||||
@@ -820,13 +820,13 @@ display_final() {
|
||||
# Bewusst KEIN "systemctl stop" von hier aus: das liefe mitten in der
|
||||
# Abschalt-Transaktion und koennte sich mit ihr verhaken. Lieber eine ehrliche
|
||||
# Zeile im Journal als ein haengendes Herunterfahren.
|
||||
echo "Warnung: Anzeigedienst laeuft nach ${DISPLAY_RELEASE_WAIT_S}s noch - kein Abschlussbild (GPIO belegt)" >&2
|
||||
echo "Warning: display service still running after ${DISPLAY_RELEASE_WAIT_S}s - no final image (GPIO busy)" >&2
|
||||
return 0
|
||||
fi
|
||||
sleep 1
|
||||
_waited=$((_waited + 1))
|
||||
done
|
||||
display_run --final "$1" || echo "Warnung: Abschlussbild konnte nicht gezeichnet werden - Anzeige pruefen" >&2
|
||||
display_run --final "$1" || echo "Warning: could not draw the final image - check the display" >&2
|
||||
return 0
|
||||
}
|
||||
|
||||
@@ -846,18 +846,18 @@ full_off() {
|
||||
ap_switch ""
|
||||
if nmcli -t --escape no -f NAME con show | grep -Fqx "$HOME_CONNECTION"; then
|
||||
if sudo nmcli con up "$HOME_CONNECTION" >/dev/null; then
|
||||
echo "Station aus, wlan0 zurueck im Heim-WLAN ($HOME_CONNECTION)"
|
||||
echo "Station off, wlan0 back in the home Wi-Fi ($HOME_CONNECTION)"
|
||||
else
|
||||
echo "Station aus. Zurueckschalten zu '$HOME_CONNECTION' fehlgeschlagen - wlan0-Status pruefen." >&2
|
||||
echo "Station off. Switching back to '$HOME_CONNECTION' failed - check the wlan0 status." >&2
|
||||
fi
|
||||
else
|
||||
echo "Station aus. Profil '$HOME_CONNECTION' fehlt, wlan0 bleibt ungenutzt - der Zugang ueber Kabel ist davon nicht betroffen." >&2
|
||||
echo "Station off. Profile '$HOME_CONNECTION' missing, wlan0 stays unused - access over the cable is unaffected." >&2
|
||||
fi
|
||||
}
|
||||
|
||||
listen_cmd() {
|
||||
write_control "listen" "" || {
|
||||
echo "Fehler: $RUN_DIR/control.json konnte nicht geschrieben werden" >&2
|
||||
echo "Error: could not write $RUN_DIR/control.json" >&2
|
||||
exit 1
|
||||
}
|
||||
echo "Phase: listen"
|
||||
@@ -882,7 +882,7 @@ if not t:
|
||||
print(t)
|
||||
PY
|
||||
); then
|
||||
echo "Fehler: kein zuletzt erkanntes Modell (last_seen) in $_status_file - Ziel angeben: ninlight-station arm <target>" >&2
|
||||
echo "Error: no recently detected model (last_seen) in $_status_file - name a target: ninlight-station arm <target>" >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
@@ -896,14 +896,14 @@ target = sys.argv[2]
|
||||
sys.exit(0 if any(p.target == target for p in products.PRODUCTS.values()) else 1)
|
||||
PY
|
||||
then
|
||||
echo "Fehler: unbekanntes Ziel '$_target' - gueltige Ziele stehen in products.py" >&2
|
||||
echo "Error: unknown target '$_target' - valid targets are listed in products.py" >&2
|
||||
exit 1
|
||||
fi
|
||||
write_control "armed" "$_target" || {
|
||||
echo "Fehler: $RUN_DIR/control.json konnte nicht geschrieben werden" >&2
|
||||
echo "Error: could not write $RUN_DIR/control.json" >&2
|
||||
exit 1
|
||||
}
|
||||
echo "Freigegeben: $_target"
|
||||
echo "Armed: $_target"
|
||||
}
|
||||
|
||||
# "renew [<minuten>]" - die Uhr zurueckstellen. Ohne Argument auf dieselbe Dauer, die
|
||||
@@ -915,7 +915,7 @@ PY
|
||||
renew_cmd() {
|
||||
_arg=${1:-}
|
||||
if ! _info=$(autooff_read); then
|
||||
echo "Fehler: kein Zeitschalter aktiv - 'renew' stellt nur einen laufenden zurueck. Einen neuen gibt es nur mit 'ninlight-station on --for <minuten>'." >&2
|
||||
echo "Error: no timer active - 'renew' only resets a running one. A new one comes from 'ninlight-station on --for <minutes>'." >&2
|
||||
exit 1
|
||||
fi
|
||||
# Absichtliches Wortsplitting: autooff_read() liefert drei Zahlen, durch Leerzeichen getrennt.
|
||||
@@ -927,17 +927,17 @@ renew_cmd() {
|
||||
_minutes=$_arg
|
||||
fi
|
||||
autooff_arm "$_minutes" "$((_minutes * 60))" 0 || exit 1
|
||||
echo "Zeitschalter zurueckgestellt: Auto-Aus in $_minutes min"
|
||||
echo "Timer reset: auto-off in $_minutes min"
|
||||
}
|
||||
|
||||
# "hold" - den Zeitschalter abbestellen. Die Station bleibt an, bis jemand "off" sagt.
|
||||
hold_cmd() {
|
||||
if ! autooff_read >/dev/null; then
|
||||
echo "Fehler: kein Zeitschalter aktiv - es gibt nichts abzubestellen." >&2
|
||||
echo "Error: no timer active - there is nothing to cancel." >&2
|
||||
exit 1
|
||||
fi
|
||||
autooff_clear
|
||||
echo "Zeitschalter abbestellt - die Station bleibt an, bis 'ninlight-station off'."
|
||||
echo "Timer cancelled - the station stays on until 'ninlight-station off'."
|
||||
}
|
||||
|
||||
# Restzeit fuer "status", oder leer, wenn kein Zeitschalter laeuft (dann steht in der
|
||||
@@ -949,9 +949,9 @@ autooff_status_line() {
|
||||
set -- $_info
|
||||
_rest=$(( ($2 - $(date +%s) + 59) / 60 ))
|
||||
if [ "$_rest" -gt 0 ]; then
|
||||
printf 'Auto-Aus: in %s min (angefordert: %s min)\n' "$_rest" "$1"
|
||||
printf 'Auto-off: in %s min (requested: %s min)\n' "$_rest" "$1"
|
||||
else
|
||||
printf 'Auto-Aus: steht an (angefordert: %s min)\n' "$1"
|
||||
printf 'Auto-off: pending (requested: %s min)\n' "$1"
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -994,8 +994,8 @@ PY
|
||||
_modus=$(printf '%s\n' "$_info" | sed -n '1p')
|
||||
_erkannt=$(printf '%s\n' "$_info" | sed -n '2p')
|
||||
elif [ -e "$_status_file" ]; then
|
||||
_modus="unbekannt (Datei nicht lesbar)"
|
||||
_erkannt="unbekannt (Datei nicht lesbar)"
|
||||
_modus="unknown (file not readable)"
|
||||
_erkannt="unknown (file not readable)"
|
||||
else
|
||||
_modus="-"
|
||||
_erkannt="-"
|
||||
@@ -1044,15 +1044,15 @@ PY
|
||||
else
|
||||
_phase="-"
|
||||
fi
|
||||
printf 'Modus : %s\n' "$_modus"
|
||||
printf 'Phase : %s\n' "$_phase"
|
||||
printf 'Mode : %s\n' "$_modus"
|
||||
printf 'Phase : %s\n' "$_phase"
|
||||
imageset_status_lines
|
||||
printf 'Erkannt: %s\n' "$_erkannt"
|
||||
printf 'Dienst : %s\n' "$(systemctl is-active ninlight-station 2>/dev/null || true)"
|
||||
printf 'Detected: %s\n' "$_erkannt"
|
||||
printf 'Service : %s\n' "$(systemctl is-active ninlight-station 2>/dev/null || true)"
|
||||
autooff_status_line
|
||||
printf 'wlan0 : %s\n' "$(nmcli -t --escape no -f GENERAL.CONNECTION device show wlan0 2>/dev/null | cut -d: -f2-)"
|
||||
printf 'Adresse: %s\n' "$(ip -4 -br addr show wlan0 2>/dev/null | awk '{print $3}')"
|
||||
echo "Images :"
|
||||
printf 'wlan0 : %s\n' "$(nmcli -t --escape no -f GENERAL.CONNECTION device show wlan0 2>/dev/null | cut -d: -f2-)"
|
||||
printf 'Address : %s\n' "$(ip -4 -br addr show wlan0 2>/dev/null | awk '{print $3}')"
|
||||
echo "Images :"
|
||||
images | while read -r f; do
|
||||
printf ' %-34s %s\n' "$(basename "$f")" \
|
||||
"$(python3 - "$f" <<'PY'
|
||||
@@ -1063,10 +1063,10 @@ PY
|
||||
)"
|
||||
done
|
||||
if [ -r "$_status_file" ]; then
|
||||
echo "Zuletzt ausgeliefert:"
|
||||
echo "Last served:"
|
||||
sed 's/^/ /' "$_status_file"
|
||||
elif [ -e "$_status_file" ]; then
|
||||
echo "Fehler: $_status_file existiert, ist aber nicht lesbar (Rechte pruefen)" >&2
|
||||
echo "Error: $_status_file exists but is not readable (check permissions)" >&2
|
||||
fi
|
||||
# "status" soll mit 0 enden, sobald es seine Auskunft geben konnte, auch wenn noch nie
|
||||
# ausgeliefert wurde - ohne das explizite exit 0 waere der Exit-Code des Skripts hier
|
||||
@@ -1082,7 +1082,7 @@ display_cmd() {
|
||||
final)
|
||||
case "${2:-}" in
|
||||
poweroff|reboot) display_final "$2" ;;
|
||||
*) echo "Fehler: 'display final' braucht 'poweroff' oder 'reboot'" >&2; exit 2 ;;
|
||||
*) echo "Error: 'display final' needs 'poweroff' or 'reboot'" >&2; exit 2 ;;
|
||||
esac
|
||||
;;
|
||||
*) usage; exit 2 ;;
|
||||
@@ -1090,48 +1090,48 @@ display_cmd() {
|
||||
}
|
||||
|
||||
usage() {
|
||||
echo "Aufruf: ninlight-station on [--for <minuten>]|recovery {on [--for <minuten>]|off}|migrate {on [--listen] [--for <minuten>]|off}|arm [<target>]|listen|bildsatz [ohne-wlan|mit-wlan]|renew [<minuten>]|hold|display {test|clear|off}|off|status" >&2
|
||||
echo "Usage: ninlight-station on [--for <minutes>]|recovery {on [--for <minutes>]|off}|migrate {on [--listen] [--for <minutes>]|off}|arm [<target>]|listen|bildsatz [ohne-wlan|mit-wlan]|renew [<minutes>]|hold|display {test|clear|off}|off|status" >&2
|
||||
echo "" >&2
|
||||
echo " bildsatz Umschalter: welcher Satz Migrationsimages ausgeliefert wird." >&2
|
||||
echo " Ohne Argument nur berichten. 'ohne-wlan' (Vorgabe) liefert die" >&2
|
||||
echo " Abzuege aus tools/build_nocreds.sh - die migrierte Lampe startet" >&2
|
||||
echo " im eigenen Onboarding-Zugangspunkt. 'mit-wlan' liefert die" >&2
|
||||
echo " Abzuege aus build/ - die Lampe haengt sofort im WLAN aus" >&2
|
||||
echo " config.local.cmake, auch wenn es eine fremde Lampe ist, die im" >&2
|
||||
echo " Automatikmodus mitgenommen wurde. Der Zustand bleibt, bis ihn" >&2
|
||||
echo " jemand aendert, und steht dauerhaft auf dem Panel." >&2
|
||||
echo " --for <minuten> Zeitschalter: die Station schaltet sich nach <minuten> selbst" >&2
|
||||
echo " wieder aus (zurueck ins Heim-WLAN). OPTIONAL - ohne '--for'" >&2
|
||||
echo " bleibt sie an, bis jemand 'off' sagt. Nuetzlich, wenn du ueber" >&2
|
||||
echo " WLAN angemeldet bist: 'on' macht wlan0 zum Zugangspunkt und" >&2
|
||||
echo " reisst die SSH-Sitzung ab. Erlaubt: 1-240." >&2
|
||||
echo " renew [<min>] Uhr zurueckstellen (ohne Angabe auf die zuletzt angeforderte" >&2
|
||||
echo " Dauer). Laeuft keiner, ist das ein Fehler." >&2
|
||||
echo " hold Zeitschalter abbestellen, Station bleibt an." >&2
|
||||
echo " display test Testbild aufs Panel zeichnen. Das ist der erste Handgriff bei der" >&2
|
||||
echo " Hardware-Abnahme und die EINZIGE Moeglichkeit, die Panel-Revision" >&2
|
||||
echo " zu bestimmen: die steht am Geraet nirgends. Bleibt das Bild leer," >&2
|
||||
echo " verzerrt oder verschoben, ist EPD_REVISION in" >&2
|
||||
echo " /etc/ninlight-station.conf falsch - auf die andere Revision" >&2
|
||||
echo " umstellen (V2 statt V3 oder umgekehrt), das Panel kurz" >&2
|
||||
echo " stromlos machen und erneut 'display test'." >&2
|
||||
echo " Ein laufender Anzeigedienst wird dafuer kurz gestoppt und" >&2
|
||||
echo " danach wieder gestartet (er haelt sonst die GPIO-Leitungen)." >&2
|
||||
echo " display clear Panel leeren (ebenfalls mit kurzer Unterbrechung des Dienstes)." >&2
|
||||
echo " bildsatz Switch: which set of migration images is served." >&2
|
||||
echo " Without an argument it only reports. 'ohne-wlan' (default) serves" >&2
|
||||
echo " the builds from tools/build_nocreds.sh - the migrated lamp comes" >&2
|
||||
echo " up in its own onboarding access point. 'mit-wlan' serves the" >&2
|
||||
echo " builds from build/ - the lamp joins the Wi-Fi from" >&2
|
||||
echo " config.local.cmake right away, including a lamp that is not" >&2
|
||||
echo " yours and got picked up in automatic mode. The setting sticks" >&2
|
||||
echo " until someone changes it, and is shown on the panel." >&2
|
||||
echo " --for <minutes> Timer: the station switches itself off after <minutes> (back to" >&2
|
||||
echo " the home Wi-Fi). OPTIONAL - without '--for' it stays on until" >&2
|
||||
echo " someone says 'off'. Useful when you are logged in over Wi-Fi:" >&2
|
||||
echo " 'on' turns wlan0 into an access point and drops your SSH" >&2
|
||||
echo " session. Allowed: 1-240." >&2
|
||||
echo " renew [<min>] Reset the timer (without an argument to the duration last" >&2
|
||||
echo " requested). If none is running, that is an error." >&2
|
||||
echo " hold Cancel the timer, station stays on." >&2
|
||||
echo " display test Draw a test image on the panel. This is the first step of the" >&2
|
||||
echo " hardware acceptance and the ONLY way to determine the panel" >&2
|
||||
echo " revision: it is printed nowhere on the device. If the image" >&2
|
||||
echo " stays blank, distorted or shifted, EPD_REVISION in" >&2
|
||||
echo " /etc/ninlight-station.conf is wrong - switch it to the other" >&2
|
||||
echo " revision (V2 instead of V3 or the other way round), power the" >&2
|
||||
echo " panel down briefly and run 'display test' again." >&2
|
||||
echo " A running display service is stopped for this and started" >&2
|
||||
echo " again afterwards (it holds the GPIO lines otherwise)." >&2
|
||||
echo " display clear Clear the panel (also with a short interruption of the service)." >&2
|
||||
echo " display final {poweroff|reboot}" >&2
|
||||
echo " Abschlussbild aufs Panel. Kein Handgriff fuer Menschen: das" >&2
|
||||
echo " rufen ninlight-poweroff.service/ninlight-reboot.service beim" >&2
|
||||
echo " Herunterfahren bzw. Neustart auf, nachdem der Anzeigedienst" >&2
|
||||
echo " die GPIO-Leitungen freigegeben hat. E-Paper behaelt sein Bild" >&2
|
||||
echo " dauerhaft - ohne diesen Schritt bliebe die letzte Rueckfrage" >&2
|
||||
echo " auf dem Panel stehen. Die Texte unterscheiden sich: beim" >&2
|
||||
echo " Neustart darf man den Stecker gerade NICHT ziehen." >&2
|
||||
echo " display off Anzeigedienst dauerhaft stoppen und das Panel leeren. 'on'/'off'" >&2
|
||||
echo " der Station brauchen das nicht - der Anzeigedienst laeuft" >&2
|
||||
echo " unabhaengig davon durch und zeichnet den Zustand selbst." >&2
|
||||
echo " Final image on the panel. Not a step for humans: this is what" >&2
|
||||
echo " ninlight-poweroff.service/ninlight-reboot.service call during" >&2
|
||||
echo " shutdown or reboot, once the display service has released the" >&2
|
||||
echo " GPIO lines. E-paper keeps its image forever - without this step" >&2
|
||||
echo " the last confirmation prompt would stay on the panel. The two" >&2
|
||||
echo " texts differ: on a reboot you must NOT unplug." >&2
|
||||
echo " display off Stop the display service for good and clear the panel. The" >&2
|
||||
echo " station's 'on'/'off' do not need this - the display service" >&2
|
||||
echo " keeps running and draws the state by itself." >&2
|
||||
echo "" >&2
|
||||
echo " Taster (GPIO23): kurzer Druck oeffnet am Geraet ein Menue ueber genau diese" >&2
|
||||
echo " Unterbefehle, 3 s gehalten brechen eine laufende Migration/Rettung ab." >&2
|
||||
echo " Button (GPIO23): a short press opens a menu on the device covering exactly" >&2
|
||||
echo " these subcommands, holding 3 s aborts a running migration or" >&2
|
||||
echo " recovery." >&2
|
||||
}
|
||||
|
||||
_cmd=${1:-}
|
||||
|
||||
@@ -167,43 +167,43 @@ def _labels(state):
|
||||
|
||||
|
||||
def test_menu_off_offers_starting_and_shutdown():
|
||||
assert _labels(STATE_OFF) == ["Rettung starten", "Migration: horchen",
|
||||
"Migration: AUTOMATIK", "Umschalten: mit WLAN",
|
||||
"Herunterfahren"]
|
||||
assert _labels(STATE_OFF) == ["Start recovery", "Migration: listen",
|
||||
"Migration: AUTOMATIC", "Switch to: with Wi-Fi",
|
||||
"Shut down"]
|
||||
|
||||
|
||||
def test_the_listening_migration_comes_before_the_automatic_one():
|
||||
"""Der horchende Modus ist der erste und naheliegende Eintrag - wer nur blaettert und
|
||||
irgendwo loslaesst, soll nicht im Automatikmodus landen."""
|
||||
labels = _labels(STATE_OFF)
|
||||
assert labels.index("Migration: horchen") < labels.index("Migration: AUTOMATIK")
|
||||
assert labels.index("Migration: listen") < labels.index("Migration: AUTOMATIC")
|
||||
|
||||
|
||||
def test_menu_in_recovery_offers_stopping_refreshing_and_shutdown():
|
||||
assert _labels(STATE_RECOVERY) == ["Station aus", "Umschalten: mit WLAN",
|
||||
"Anzeige auffrischen", "Herunterfahren"]
|
||||
assert _labels(STATE_RECOVERY) == ["Station off", "Switch to: with Wi-Fi",
|
||||
"Refresh display", "Shut down"]
|
||||
|
||||
|
||||
def test_menu_in_migration_offers_the_detected_target():
|
||||
assert _labels(STATE_MIGRATION) == ["Freigabe: gu10_rgb", "Station aus",
|
||||
"Umschalten: mit WLAN", "Anzeige auffrischen",
|
||||
"Herunterfahren"]
|
||||
assert _labels(STATE_MIGRATION) == ["Arm: gu10_rgb", "Station off",
|
||||
"Switch to: with Wi-Fi", "Refresh display",
|
||||
"Shut down"]
|
||||
|
||||
|
||||
def test_menu_in_migration_without_a_detected_target_offers_no_release():
|
||||
"""Ohne last_seen gibt es nichts freizugeben - der Eintrag faellt ganz weg, statt
|
||||
eine Freigabe ins Leere anzubieten (die das CLI ohnehin abweisen wuerde)."""
|
||||
assert "Freigabe: " not in " ".join(_labels(STATE_MIGRATION_BLIND))
|
||||
assert _labels(STATE_MIGRATION_BLIND) == ["Station aus", "Umschalten: mit WLAN",
|
||||
"Anzeige auffrischen", "Herunterfahren"]
|
||||
assert "Armed: " not in " ".join(_labels(STATE_MIGRATION_BLIND))
|
||||
assert _labels(STATE_MIGRATION_BLIND) == ["Station off", "Switch to: with Wi-Fi",
|
||||
"Refresh display", "Shut down"]
|
||||
|
||||
|
||||
def test_only_the_refresh_entry_skips_the_confirmation():
|
||||
"""Zusage: JEDE veraendernde Auswahl bekommt eine Rueckfrage. "Anzeige auffrischen"
|
||||
"""Zusage: JEDE veraendernde Auswahl bekommt eine Rueckfrage. "Refresh display"
|
||||
veraendert nichts und braucht keine."""
|
||||
for state in (STATE_OFF, STATE_RECOVERY, STATE_MIGRATION):
|
||||
for action in button.menu_actions(state):
|
||||
assert action.confirm is (action.label != "Anzeige auffrischen"), action.label
|
||||
assert action.confirm is (action.label != "Refresh display"), action.label
|
||||
|
||||
|
||||
def test_actions_run_through_the_station_cli():
|
||||
@@ -222,7 +222,7 @@ def test_a_short_press_opens_the_menu():
|
||||
rec = _drive([_short(1.0)], 4.0)
|
||||
assert rec.argvs() == []
|
||||
t, view = rec.first_view("menu")
|
||||
assert view["hero"] == "> Rettung starten"
|
||||
assert view["hero"] == "> Start recovery"
|
||||
assert view["active"] is True
|
||||
# Spuerbar, aber bedienbar: das Menue steht binnen eines Schleifentakts nach dem
|
||||
# Loslassen (die Verzoegerung am Panel kommt aus dessen eigenem Takt, siehe
|
||||
@@ -233,19 +233,19 @@ def test_a_short_press_opens_the_menu():
|
||||
def test_a_short_press_steps_to_the_next_entry():
|
||||
rec = _drive([_short(1.0), _short(2.0)], 4.0)
|
||||
assert rec.argvs() == []
|
||||
assert rec.views()[-1][1]["hero"] == "> Migration: horchen"
|
||||
assert rec.views()[-1][1]["hero"] == "> Migration: listen"
|
||||
|
||||
|
||||
def test_a_short_press_wraps_around_the_last_entry():
|
||||
rec = _drive([_short(1.0), _short(2.0), _short(3.0), _short(4.0), _short(5.0),
|
||||
_short(6.0)], 8.0)
|
||||
assert rec.views()[-1][1]["hero"] == "> Rettung starten"
|
||||
assert rec.views()[-1][1]["hero"] == "> Start recovery"
|
||||
|
||||
|
||||
def test_holding_one_and_a_half_seconds_selects_the_entry():
|
||||
rec = _drive([_short(1.0), (2.0, 4.0)], 6.0)
|
||||
t, view = rec.first_view("confirm")
|
||||
assert view["hero"] == "Rettung starten"
|
||||
assert view["hero"] == "Start recovery"
|
||||
assert 3.5 <= t <= 3.6
|
||||
# Die Auswahl allein fuehrt noch nichts aus - dafuer ist die Rueckfrage da.
|
||||
assert rec.argvs() == []
|
||||
@@ -271,7 +271,7 @@ def test_the_refresh_entry_runs_without_a_confirmation():
|
||||
|
||||
|
||||
def test_the_listening_entry_calls_the_cli_with_listen():
|
||||
"""Der zweite Eintrag ("Migration: horchen") ruft das CLI MIT --listen: der
|
||||
"""Der zweite Eintrag ("Migration: listen") ruft das CLI MIT --listen: der
|
||||
Zugangspunkt steht, es geht aber nichts raus, bis jemand eine erkannte Lampe
|
||||
freigibt."""
|
||||
rec = _drive([_short(1.0), _short(2.0), (3.0, 5.0), (6.0, 8.0)], 10.0)
|
||||
@@ -280,7 +280,7 @@ def test_the_listening_entry_calls_the_cli_with_listen():
|
||||
|
||||
|
||||
def test_the_automatic_entry_calls_the_cli_without_listen():
|
||||
"""Der dritte Eintrag ("Migration: AUTOMATIK") ruft dasselbe CLI OHNE --listen - das
|
||||
"""Der dritte Eintrag ("Migration: AUTOMATIC") ruft dasselbe CLI OHNE --listen - das
|
||||
ist der ganze Unterschied zwischen den beiden Eintraegen, und er entscheidet, ob
|
||||
fremde Lampen ungefragt migriert werden."""
|
||||
rec = _drive([_short(1.0), _short(2.0), _short(3.0), (4.0, 6.0), (7.0, 9.0)], 11.0)
|
||||
@@ -293,18 +293,18 @@ def test_the_automatic_entry_asks_a_louder_question_than_the_others():
|
||||
zu fragen - und sie unterscheidet sich sichtbar von der der uebrigen Eintraege."""
|
||||
rec = _drive([_short(1.0), _short(2.0), _short(3.0), (4.0, 6.0)], 8.0)
|
||||
_t, view = rec.first_view("confirm")
|
||||
assert view["title"] == "ACHTUNG"
|
||||
assert view["hero"] == "Migration: AUTOMATIK"
|
||||
assert view["title"] == "WARNING"
|
||||
assert view["hero"] == "Migration: AUTOMATIC"
|
||||
text = " ".join(view["details"]).lower()
|
||||
assert "jede" in text and "fremde" in text
|
||||
assert "every" in text and "other people" in text
|
||||
# Die Bedienhinweise bleiben trotzdem stehen, und zwar zuletzt.
|
||||
assert view["details"][-2:] == ["halten (1,5 s): ja", "kurz: nein"]
|
||||
assert view["details"][-2:] == ["hold (1.5 s): yes", "tap: no"]
|
||||
|
||||
# Gegenprobe am horchenden Eintrag: uebliche Rueckfrage, keine Warnung.
|
||||
rec = _drive([_short(1.0), _short(2.0), (3.0, 5.0)], 7.0)
|
||||
_t, plain = rec.first_view("confirm")
|
||||
assert plain["title"] == "RUECKFRAGE"
|
||||
assert plain["details"] == ["halten (1,5 s): ja", "kurz: nein"]
|
||||
assert plain["title"] == "CONFIRM"
|
||||
assert plain["details"] == ["hold (1.5 s): yes", "tap: no"]
|
||||
|
||||
|
||||
def test_the_cancel_fires_at_three_seconds_while_the_button_is_still_held():
|
||||
@@ -316,7 +316,7 @@ def test_the_cancel_fires_at_three_seconds_while_the_button_is_still_held():
|
||||
assert 4.0 <= t <= 4.1
|
||||
# ... und danach laeuft am Panel der Countdown der zweiten Stufe.
|
||||
later = [v for vt, v in rec.views() if vt > t]
|
||||
assert any("Herunterfahren in" in v.get("hero", "") for v in later)
|
||||
assert any("Shutting down in" in v.get("hero", "") for v in later)
|
||||
|
||||
|
||||
def test_releasing_between_three_and_ten_seconds_does_not_shut_down():
|
||||
@@ -337,8 +337,8 @@ def test_the_shutdown_countdown_is_visible_before_it_happens():
|
||||
erreichbar - der Countdown muss also vorher auf dem Panel gestanden haben."""
|
||||
rec = _drive([(1.0, 12.0)], 14.0)
|
||||
heroes = [v.get("hero", "") for t, v in rec.views() if t < 11.0]
|
||||
assert "Herunterfahren in 6 s" in heroes
|
||||
assert "Herunterfahren in 1 s" in heroes
|
||||
assert "Shutting down in 6 s" in heroes
|
||||
assert "Shutting down in 1 s" in heroes
|
||||
|
||||
|
||||
def test_a_blocking_action_does_not_break_the_hold_gesture():
|
||||
@@ -392,7 +392,7 @@ def test_an_expired_confirmation_leaves_nothing_pending():
|
||||
|
||||
def test_a_failed_action_says_so_on_the_panel():
|
||||
rec = _drive([_short(1.0), (2.0, 4.0), (5.0, 7.0)], 9.0, rc=1)
|
||||
assert any(v.get("view") == "result" and v.get("title") == "FEHLER"
|
||||
assert any(v.get("view") == "result" and v.get("title") == "ERROR"
|
||||
for _t, v in rec.views())
|
||||
|
||||
|
||||
@@ -411,7 +411,7 @@ def test_the_ui_file_is_written_atomically_with_0644(tmp_path):
|
||||
geschrieben."""
|
||||
path = tmp_path / "run" / "ui.json"
|
||||
writer = button.UiWriter(path)
|
||||
view = {"view": "menu", "title": "MENUE", "hero": "> Station aus", "details": [],
|
||||
view = {"view": "menu", "title": "MENU", "hero": "> Station off", "details": [],
|
||||
"active": True}
|
||||
# Unter einer strengen umask geschrieben: die Rechte muessen ausdruecklich gesetzt
|
||||
# werden, nicht zufaellig aus der umask des Dienstes fallen - sonst laege die Zusage
|
||||
@@ -509,10 +509,10 @@ def test_an_unreadable_station_state_keeps_the_menu_closed(caplog):
|
||||
def test_the_image_set_entry_names_where_it_switches_to():
|
||||
"""Waehrend das Menue offen ist, ersetzt es das Statusbild samt Fusszeile - der
|
||||
Eintrag muss deshalb das ZIEL nennen, nicht den jetzigen Zustand."""
|
||||
assert "Umschalten: mit WLAN" in _labels(STATE_OFF)
|
||||
assert "Switch to: with Wi-Fi" in _labels(STATE_OFF)
|
||||
ohne = dict(STATE_OFF, imageset="mit-wlan")
|
||||
assert "Umschalten: ohne WLAN" in _labels(ohne)
|
||||
assert "Umschalten: mit WLAN" not in _labels(ohne)
|
||||
assert "Switch to: no Wi-Fi" in _labels(ohne)
|
||||
assert "Switch to: with Wi-Fi" not in _labels(ohne)
|
||||
|
||||
|
||||
def test_the_image_set_entry_switches_through_the_cli():
|
||||
@@ -527,9 +527,9 @@ def test_switching_to_wifi_credentials_asks_louder_than_switching_back():
|
||||
loud = button.action_imageset("ohne-wlan")
|
||||
back = button.action_imageset("mit-wlan")
|
||||
assert loud.confirm and back.confirm
|
||||
assert loud.confirm_title == "ACHTUNG"
|
||||
assert back.confirm_title == "RUECKFRAGE"
|
||||
assert "WLAN-Passwort" in " ".join(loud.confirm_details)
|
||||
assert loud.confirm_title == "WARNING"
|
||||
assert back.confirm_title == "CONFIRM"
|
||||
assert "Wi-Fi password" in " ".join(loud.confirm_details)
|
||||
assert back.confirm_details is None
|
||||
|
||||
|
||||
|
||||
@@ -364,7 +364,7 @@ set -e
|
||||
grep -q "nmcli con up ninlight-recovery" "$SBX/cmd.log" && pass "nmcli con up ninlight-recovery aufgerufen" || fail "nmcli con up ninlight-recovery fehlt im Log"
|
||||
grep -q "systemctl restart ninlight-station" "$SBX/cmd.log" && pass "Dienst (neu) gestartet" || fail "kein systemctl restart im Log"
|
||||
[ "$(cat "$SBX/run/mode")" = "recovery" ] && pass "Modusdatei enthaelt 'recovery'" || fail "Modusdatei falsch: $(cat "$SBX/run/mode" 2>&1)"
|
||||
echo "$OUT" | grep -q "Station an" && pass "Ausgabe enthaelt 'Station an'" || fail "Ausgabe fehlt 'Station an': $OUT"
|
||||
echo "$OUT" | grep -q "Station on" && pass "Ausgabe enthaelt 'Station an'" || fail "Ausgabe fehlt 'Station an': $OUT"
|
||||
|
||||
echo
|
||||
echo "=== Fall 2: 'recovery on' identisch zu 'on' (Rueckwaertskompatibilitaet) ==="
|
||||
@@ -378,7 +378,7 @@ set -e
|
||||
grep -q "nmcli con up ninlight-recovery" "$SBX/cmd.log" && pass "nmcli con up ninlight-recovery aufgerufen" || fail "nmcli con up ninlight-recovery fehlt im Log"
|
||||
grep -q "systemctl restart ninlight-station" "$SBX/cmd.log" && pass "Dienst (neu) gestartet" || fail "kein systemctl restart im Log"
|
||||
[ "$(cat "$SBX/run/mode")" = "recovery" ] && pass "Modusdatei enthaelt 'recovery'" || fail "Modusdatei falsch"
|
||||
echo "$OUT" | grep -q "Station an" && pass "Ausgabe enthaelt 'Station an'" || fail "Ausgabe fehlt 'Station an': $OUT"
|
||||
echo "$OUT" | grep -q "Station on" && pass "Ausgabe enthaelt 'Station an'" || fail "Ausgabe fehlt 'Station an': $OUT"
|
||||
|
||||
echo
|
||||
echo "=== Fall 3: 'migrate on' faehrt ninlight-recovery herunter, BEVOR qconnex-firmware hochkommt; control.json phase=auto ==="
|
||||
@@ -427,7 +427,7 @@ RC=$?
|
||||
set -e
|
||||
ERR=$(cat "$SBX/stderr.log")
|
||||
[ "$RC" -eq 0 ] && pass "Exit-Code 0 trotz fehlender Migrationsimages" || fail "Exit-Code $RC, stderr: $ERR"
|
||||
echo "$ERR" | grep -q "Warnung" && pass "Warnung steht auf stderr" || fail "keine Warnung auf stderr: $ERR"
|
||||
echo "$ERR" | grep -q "Warning" && pass "Warnung steht auf stderr" || fail "keine Warnung auf stderr: $ERR"
|
||||
|
||||
echo
|
||||
echo "=== Fall 6: 'arm' ohne Argument liest last_seen.target aus status.json ==="
|
||||
@@ -469,7 +469,7 @@ RC=$?
|
||||
set -e
|
||||
ERR=$(cat "$SBX/stderr.log")
|
||||
[ "$RC" -ne 0 ] && pass "Exit-Code != 0 ($RC)" || fail "Exit-Code war 0"
|
||||
echo "$ERR" | grep -qi "fehler" && pass "Fehlermeldung steht auf stderr" || fail "keine Fehlermeldung auf stderr: $ERR"
|
||||
echo "$ERR" | grep -qi "error" && pass "Fehlermeldung steht auf stderr" || fail "keine Fehlermeldung auf stderr: $ERR"
|
||||
[ ! -e "$SBX/run/control.json" ] && pass "control.json wurde nicht angelegt" || fail "control.json wurde trotz Fehler angelegt: $(cat "$SBX/run/control.json")"
|
||||
|
||||
echo
|
||||
@@ -482,7 +482,7 @@ RC=$?
|
||||
set -e
|
||||
ERR=$(cat "$SBX/stderr.log")
|
||||
[ "$RC" -ne 0 ] && pass "Exit-Code != 0 ($RC)" || fail "Exit-Code war 0"
|
||||
echo "$ERR" | grep -qi "fehler" && pass "Fehlermeldung steht auf stderr" || fail "keine Fehlermeldung auf stderr: $ERR"
|
||||
echo "$ERR" | grep -qi "error" && pass "Fehlermeldung steht auf stderr" || fail "keine Fehlermeldung auf stderr: $ERR"
|
||||
[ ! -e "$SBX/run/control.json" ] && pass "control.json wurde nicht angelegt" || fail "control.json wurde trotz Fehler angelegt"
|
||||
|
||||
echo
|
||||
@@ -513,7 +513,7 @@ ACTIVE=$(cat "$SBX/nmstate/active")
|
||||
echo "$ACTIVE" | grep -qx "ninlight-recovery" && fail "ninlight-recovery ist noch aktiv" || pass "ninlight-recovery ist nicht mehr aktiv"
|
||||
echo "$ACTIVE" | grep -qx "qconnex-firmware" && fail "qconnex-firmware ist noch aktiv" || pass "qconnex-firmware ist nicht mehr aktiv"
|
||||
echo "$ACTIVE" | grep -qx "home-wifi" && pass "zurueck im Heim-WLAN (home-wifi aktiv)" || fail "home-wifi ist nicht aktiv: $ACTIVE"
|
||||
echo "$OUT" | grep -q "Heim-WLAN" && pass "Ausgabe erwaehnt Heim-WLAN" || fail "Ausgabe erwaehnt Heim-WLAN nicht: $OUT"
|
||||
echo "$OUT" | grep -q "home Wi-Fi" && pass "Ausgabe erwaehnt Heim-WLAN" || fail "Ausgabe erwaehnt Heim-WLAN nicht: $OUT"
|
||||
|
||||
echo
|
||||
echo "=== Fall 12: 'status' im Migrationsmodus zeigt Modus, Phase und zuletzt erkannte Lampe ==="
|
||||
@@ -531,9 +531,9 @@ OUT=$(run_cli "$SBX" status 2>&1)
|
||||
RC=$?
|
||||
set -e
|
||||
[ "$RC" -eq 0 ] && pass "Exit-Code 0" || fail "Exit-Code $RC, Ausgabe: $OUT"
|
||||
echo "$OUT" | grep -q "Modus.*migration" && pass "Modus wird angezeigt" || fail "Modus fehlt: $OUT"
|
||||
echo "$OUT" | grep -q "Mode.*migration" && pass "Modus wird angezeigt" || fail "Modus fehlt: $OUT"
|
||||
echo "$OUT" | grep -q "Phase.*auto" && pass "Phase wird angezeigt" || fail "Phase fehlt: $OUT"
|
||||
echo "$OUT" | grep -q "Erkannt.*Klyqa GU10 RGB CW/WW" && pass "zuletzt erkannte Lampe wird angezeigt" || fail "erkannte Lampe fehlt: $OUT"
|
||||
echo "$OUT" | grep -q "Detected.*Klyqa GU10 RGB CW/WW" && pass "zuletzt erkannte Lampe wird angezeigt" || fail "erkannte Lampe fehlt: $OUT"
|
||||
|
||||
echo
|
||||
echo "=== Fall 13: 'status' mit unlesbarer status.json meldet das ausdruecklich (4a-Fehler) ==="
|
||||
@@ -547,7 +547,7 @@ RC=$?
|
||||
set -e
|
||||
chmod 644 "$SBX/run/status.json" # damit trap-Aufraeumen am Ende sicher klappt
|
||||
[ "$RC" -eq 0 ] && pass "Exit-Code 0 (status liefert trotzdem eine Auskunft)" || fail "Exit-Code $RC, Ausgabe: $OUT"
|
||||
echo "$OUT" | grep -q "existiert, ist aber nicht lesbar" && pass "unlesbare status.json wird ausdruecklich gemeldet" || fail "Meldung zu unlesbarer status.json fehlt: $OUT"
|
||||
echo "$OUT" | grep -q "not readable" && pass "unlesbare status.json wird ausdruecklich gemeldet" || fail "Meldung zu unlesbarer status.json fehlt: $OUT"
|
||||
|
||||
echo
|
||||
echo "=== Fall 14: unbekannter Unterbefehl -> Aufrufhinweis, Rueckgabewert 2 ==="
|
||||
@@ -558,7 +558,7 @@ OUT=$(run_cli "$SBX" frobnicate 2>&1)
|
||||
RC=$?
|
||||
set -e
|
||||
[ "$RC" -eq 2 ] && pass "Exit-Code 2" || fail "Exit-Code war $RC statt 2"
|
||||
echo "$OUT" | grep -qi "Aufruf" && pass "Aufrufhinweis vorhanden" || fail "kein Aufrufhinweis: $OUT"
|
||||
echo "$OUT" | grep -qi "usage" && pass "Aufrufhinweis vorhanden" || fail "kein Aufrufhinweis: $OUT"
|
||||
|
||||
echo
|
||||
echo "=== Fall 15 (wichtigster Test): 'migrate on' bei laufendem Recovery-Modus laesst GENAU EIN Profil aktiv ==="
|
||||
@@ -597,7 +597,7 @@ set -e
|
||||
ERR=$(cat "$SBX/stderr.log")
|
||||
[ "$RC" -ne 0 ] && pass "Exit-Code != 0 - eine geleerte RuntimeDirectory wird nicht als Erfolg gemeldet" || fail "Exit-Code war 0, obwohl RUN_DIR beim Neustart geleert wurde: $OUT"
|
||||
echo "$ERR" | grep -qi "RuntimeDirectoryPreserve" && pass "Fehlermeldung nennt RuntimeDirectoryPreserve als Ursache" || fail "Fehlermeldung nennt die Ursache nicht: $ERR"
|
||||
echo "$OUT" | grep -q "Migration an" && fail "CLI meldete faelschlich 'Migration an', obwohl die Phase nach dem Neustart nicht mehr stimmte" || pass "kein falscher Erfolgsmeldung 'Migration an'"
|
||||
echo "$OUT" | grep -q "Migration on" && fail "CLI meldete faelschlich 'Migration an', obwohl die Phase nach dem Neustart nicht mehr stimmte" || pass "kein falscher Erfolgsmeldung 'Migration an'"
|
||||
ACTIVE_AFTER=$(cat "$SBX/nmstate/active")
|
||||
[ -z "$ACTIVE_AFTER" ] && pass "Zugangspunkt wurde nach dem erkannten Fehler wieder heruntergefahren" || fail "Zugangspunkt blieb aktiv: $ACTIVE_AFTER"
|
||||
# Gegenprobe: dieselbe Sandbox, aber mit der ECHTEN (reparierten) Unit-Datei -
|
||||
@@ -646,8 +646,8 @@ RC=$?
|
||||
set -e
|
||||
ERR=$(cat "$SBX/stderr.log")
|
||||
[ "$RC" -ne 0 ] && pass "Exit-Code != 0 - ein haengender Zugangspunkt wird nicht als 'Station aus' gemeldet" || fail "Exit-Code war 0, obwohl ninlight-recovery weiterhin aktiv war: $OUT"
|
||||
echo "$OUT" | grep -q "Station aus" && fail "CLI behauptete faelschlich 'Station aus'" || pass "keine falsche 'Station aus'-Meldung"
|
||||
echo "$ERR" | grep -qi "fehler" && pass "Fehlermeldung auf stderr vorhanden" || fail "keine Fehlermeldung auf stderr: $ERR"
|
||||
echo "$OUT" | grep -q "Station off" && fail "CLI behauptete faelschlich 'Station aus'" || pass "keine falsche 'Station aus'-Meldung"
|
||||
echo "$ERR" | grep -qi "error" && pass "Fehlermeldung auf stderr vorhanden" || fail "keine Fehlermeldung auf stderr: $ERR"
|
||||
ACTIVE_AFTER=$(cat "$SBX/nmstate/active")
|
||||
[ "$ACTIVE_AFTER" = "ninlight-recovery" ] && pass "ninlight-recovery ist (wie vom Stub simuliert) tatsaechlich noch aktiv" || fail "unerwarteter nmcli-Zustand: $ACTIVE_AFTER"
|
||||
|
||||
@@ -679,7 +679,7 @@ ERR=$(cat "$SBX/stderr.log")
|
||||
[ "$RC" -ne 0 ] \
|
||||
&& pass "Exit-Code != 0 - der haengende Zugangspunkt wird trotz Doppelpunkt im Namen erkannt" \
|
||||
|| fail "Exit-Code war 0, obwohl 'ninlight:recovery' weiterhin aktiv war: $OUT"
|
||||
echo "$OUT" | grep -q "Station aus" && fail "CLI behauptete faelschlich 'Station aus'" \
|
||||
echo "$OUT" | grep -q "Station off" && fail "CLI behauptete faelschlich 'Station aus'" \
|
||||
|| pass "keine falsche 'Station aus'-Meldung"
|
||||
echo "$ERR" | grep -q "ninlight:recovery" \
|
||||
&& pass "die Fehlermeldung nennt den vollstaendigen Profilnamen, nicht den abgeschnittenen" \
|
||||
@@ -695,7 +695,7 @@ RC=$?
|
||||
set -e
|
||||
ERR=$(cat "$SBX/stderr.log")
|
||||
[ "$RC" -ne 0 ] && pass "Exit-Code != 0, wenn wlan0 nach dem Umschalten ein fremdes Profil zeigt" || fail "Exit-Code war 0 trotz Profil-Mismatch: $OUT"
|
||||
echo "$ERR" | grep -q "unerwartet" && pass "Fehlermeldung nennt den Mismatch" || fail "Fehlermeldung fehlt: $ERR"
|
||||
echo "$ERR" | grep -q "unexpectedly" && pass "Fehlermeldung nennt den Mismatch" || fail "Fehlermeldung fehlt: $ERR"
|
||||
ACTIVE_AFTER=$(cat "$SBX/nmstate/active")
|
||||
[ -z "$ACTIVE_AFTER" ] && pass "beide Stationsprofile wurden nach dem erkannten Mismatch heruntergefahren" || fail "mindestens ein Profil blieb aktiv: $ACTIVE_AFTER"
|
||||
|
||||
@@ -714,7 +714,7 @@ cat >"$SBX/run/status.json" <<'JSON'
|
||||
{"mode": "recovery"}
|
||||
JSON
|
||||
OUT=$(run_cli "$SBX" status 2>&1)
|
||||
echo "$OUT" | grep -qE "Modus[[:space:]]*: recovery" && pass "Modus zeigt korrekt 'recovery'" || fail "Modus falsch: $OUT"
|
||||
echo "$OUT" | grep -qE "Mode[[:space:]]*: recovery" && pass "Modus zeigt korrekt 'recovery'" || fail "Modus falsch: $OUT"
|
||||
echo "$OUT" | grep -qi "armed" && fail "status zeigt weiterhin die veraltete Migrationsphase 'armed' im Rettungsmodus: $OUT" || pass "keine veraltete Migrationsphase 'armed' im Rettungsmodus"
|
||||
echo "$OUT" | grep -qE "Phase[[:space:]]*: -" && pass "Phase zeigt den neutralen Platzhalter '-' ausserhalb des Migrationsmodus" || fail "Phase zeigt keinen neutralen Platzhalter: $OUT"
|
||||
|
||||
@@ -728,7 +728,7 @@ run_cli "$SBX" arm gu10_rgb >/dev/null 2>&1
|
||||
run_cli "$SBX" on >/dev/null 2>&1
|
||||
rm -f "$SBX/run/status.json" # kein Serverprozess in diesem Test aktualisiert sie
|
||||
OUT=$(run_cli "$SBX" status 2>&1)
|
||||
echo "$OUT" | grep -qE "Modus[[:space:]]*: -" && pass "Modus zeigt '-' (keine status.json vorhanden)" || fail "Modus unerwartet: $OUT"
|
||||
echo "$OUT" | grep -qE "Mode[[:space:]]*: -" && pass "Modus zeigt '-' (keine status.json vorhanden)" || fail "Modus unerwartet: $OUT"
|
||||
echo "$OUT" | grep -qi "armed" && fail "status zeigt 'armed', obwohl der Modus gar nicht bekannt ist: $OUT" || pass "keine 'armed'-Phase bei unbekanntem Modus"
|
||||
echo "$OUT" | grep -qE "Phase[[:space:]]*: -" && pass "Phase zeigt '-' statt einer Vermutung" || fail "Phase zeigt keinen neutralen Platzhalter: $OUT"
|
||||
|
||||
@@ -1024,9 +1024,9 @@ set -e
|
||||
[ "$RC" -eq 0 ] && pass "Exit-Code 0" || fail "Exit-Code $RC, Ausgabe: $OUT"
|
||||
[ ! -e "$SBX/run/autooff.json" ] && pass "keine Zeitschalter-Zustandsdatei angelegt" || fail "ohne --for entstand trotzdem ein Zeitschalter: $(cat "$SBX/run/autooff.json")"
|
||||
grep -q "systemd-run" "$SBX/cmd.log" && fail "ohne --for wurde trotzdem ein Timer angelegt: $(grep systemd-run "$SBX/cmd.log")" || pass "kein systemd-run-Aufruf"
|
||||
echo "$OUT" | grep -qi "Auto-Aus" && fail "Ausgabe verspricht einen Zeitschalter, den es nicht gibt: $OUT" || pass "Ausgabe erwaehnt keinen Zeitschalter"
|
||||
echo "$OUT" | grep -qi "Auto-off" && fail "Ausgabe verspricht einen Zeitschalter, den es nicht gibt: $OUT" || pass "Ausgabe erwaehnt keinen Zeitschalter"
|
||||
OUT=$(run_cli "$SBX" status 2>&1)
|
||||
echo "$OUT" | grep -qi "Auto-Aus" && fail "'status' zeigt eine Zeitschalter-Zeile, obwohl keiner laeuft: $OUT" || pass "'status' schweigt ueber den Zeitschalter"
|
||||
echo "$OUT" | grep -qi "Auto-off" && fail "'status' zeigt eine Zeitschalter-Zeile, obwohl keiner laeuft: $OUT" || pass "'status' schweigt ueber den Zeitschalter"
|
||||
|
||||
echo
|
||||
echo "=== Fall Z2: 'on --for 30' legt einen Zeitschalter an ==="
|
||||
@@ -1046,7 +1046,7 @@ grep -q -- "--on-active=1800s" "$SBX/cmd.log" && pass "Timer feuert nach 30 min"
|
||||
grep -q "__autooff" "$SBX/cmd.log" && pass "Timer ruft das CLI mit seiner Abschaltaktion" || fail "Timer ruft nichts Abschaltendes: $(grep systemd-run "$SBX/cmd.log")"
|
||||
[ "$(cat "$SBX/run/mode")" = "recovery" ] && pass "die Station ist trotzdem ganz normal an" || fail "Modusdatei falsch"
|
||||
OUT=$(run_cli "$SBX" status 2>&1)
|
||||
echo "$OUT" | grep -qE "Auto-Aus: in (29|30) min" && pass "'status' zeigt die Restzeit" || fail "'status' zeigt keine brauchbare Restzeit: $OUT"
|
||||
echo "$OUT" | grep -qE "Auto-off: in (29|30) min" && pass "'status' zeigt die Restzeit" || fail "'status' zeigt keine brauchbare Restzeit: $OUT"
|
||||
|
||||
echo
|
||||
echo "=== Fall Z3: ungueltige Minutenangaben schalten GAR NICHTS ein ==="
|
||||
@@ -1070,7 +1070,7 @@ for BAD in "0" "241" "abc" "-5" "1.5" ""; do
|
||||
set -e
|
||||
ERR=$(cat "$SBX/stderr.log")
|
||||
[ "$RC" -ne 0 ] && pass "'--for ${BAD:-<leer>}' wird abgelehnt (Exit $RC)" || fail "'--for ${BAD:-<leer>}' wurde angenommen: $OUT"
|
||||
echo "$ERR" | grep -qi "fehler" && pass "'--for ${BAD:-<leer>}': Fehlermeldung auf stderr" || fail "'--for ${BAD:-<leer>}': keine Fehlermeldung: $ERR"
|
||||
echo "$ERR" | grep -qi "error" && pass "'--for ${BAD:-<leer>}': Fehlermeldung auf stderr" || fail "'--for ${BAD:-<leer>}': keine Fehlermeldung: $ERR"
|
||||
grep -q "nmcli con up" "$SBX/cmd.log" && fail "'--for ${BAD:-<leer>}' hat trotzdem einen Zugangspunkt hochgefahren: $(cat "$SBX/cmd.log")" || pass "'--for ${BAD:-<leer>}': kein Zugangspunkt hochgefahren"
|
||||
grep -q "systemctl restart" "$SBX/cmd.log" && fail "'--for ${BAD:-<leer>}' hat den Dienst gestartet" || pass "'--for ${BAD:-<leer>}': Dienst nicht gestartet"
|
||||
[ ! -e "$SBX/run/mode" ] && pass "'--for ${BAD:-<leer>}': keine Modusdatei geschrieben" || fail "'--for ${BAD:-<leer>}' hat eine Modusdatei geschrieben: $(cat "$SBX/run/mode")"
|
||||
@@ -1115,7 +1115,7 @@ grep -q "systemctl stop ninlight-station$" "$SBX/cmd.log" && fail "die Station w
|
||||
grep -q "nmcli con down" "$SBX/cmd.log" && fail "der Zugangspunkt wurde mitten in der Auslieferung heruntergefahren" || pass "der Zugangspunkt bleibt oben"
|
||||
[ "$(autooff_field "$SBX/run/autooff.json" deferrals)" = "1" ] && pass "Verschiebung gezaehlt (1)" || fail "deferrals nicht hochgezaehlt: $(cat "$SBX/run/autooff.json" 2>&1)"
|
||||
[ "$(autooff_field "$SBX/run/autooff.json" minutes)" = "30" ] && pass "die angeforderte Dauer bleibt fuer 'renew' erhalten" || fail "minutes wurde ueberschrieben: $(cat "$SBX/run/autooff.json")"
|
||||
echo "$OUT" | grep -qi "verschoben" && pass "die Verschiebung wird gemeldet" || fail "keine Meldung ueber die Verschiebung: $OUT"
|
||||
echo "$OUT" | grep -qi "deferred" && pass "die Verschiebung wird gemeldet" || fail "keine Meldung ueber die Verschiebung: $OUT"
|
||||
# Gegenprobe: dieselbe Lage ohne laufende Verbindung schaltet sehr wohl ab.
|
||||
SBX="$TMPROOT/tZ5b"
|
||||
setup_sandbox "$SBX"
|
||||
@@ -1149,7 +1149,7 @@ set -e
|
||||
ERR=$(cat "$SBX/stderr.log")
|
||||
[ "$RC" -eq 0 ] && pass "Exit-Code 0" || fail "Exit-Code $RC: $OUT $ERR"
|
||||
grep -q "systemctl stop ninlight-station$" "$SBX/cmd.log" && pass "an der Obergrenze wird trotz offener Verbindung abgeschaltet" || fail "die haengende Verbindung hebelt den Zeitschalter dauerhaft aus: $(cat "$SBX/cmd.log")"
|
||||
echo "$ERR" | grep -qi "warnung" && pass "das Abschalten trotz offener Verbindung wird gemeldet" || fail "keine Warnung: $ERR"
|
||||
echo "$ERR" | grep -qi "warning" && pass "das Abschalten trotz offener Verbindung wird gemeldet" || fail "keine Warnung: $ERR"
|
||||
[ ! -e "$SBX/run/autooff.json" ] && pass "Zustandsdatei abgeraeumt" || fail "Zustandsdatei blieb liegen"
|
||||
# Eine Verschiebung UNTER der Grenze verschiebt dagegen weiter (sonst koennte die
|
||||
# Grenzpruefung auch einfach immer abschalten).
|
||||
@@ -1195,7 +1195,7 @@ for CMD in renew hold; do
|
||||
set -e
|
||||
ERR=$(cat "$SBX/stderr.log")
|
||||
[ "$RC" -ne 0 ] && pass "'$CMD' ohne Zeitschalter: Exit-Code $RC" || fail "'$CMD' meldete Erfolg: $OUT"
|
||||
echo "$ERR" | grep -qi "kein Zeitschalter" && pass "'$CMD': klare Meldung auf stderr" || fail "'$CMD': unklare Meldung: $ERR"
|
||||
echo "$ERR" | grep -qi "no timer active" && pass "'$CMD': klare Meldung auf stderr" || fail "'$CMD': unklare Meldung: $ERR"
|
||||
[ ! -e "$SBX/run/autooff.json" ] && pass "'$CMD' hat keinen Zeitschalter angelegt" || fail "'$CMD' hat stillschweigend einen angelegt: $(cat "$SBX/run/autooff.json")"
|
||||
grep -q "systemd-run" "$SBX/cmd.log" && fail "'$CMD' hat einen Timer angelegt" || pass "'$CMD': kein systemd-run"
|
||||
done
|
||||
@@ -1215,7 +1215,7 @@ set -e
|
||||
[ "$(autooff_field "$SBX/run/autooff.json" minutes)" = "30" ] && pass "dieselbe Dauer wie zuletzt angefordert (30)" || fail "minutes falsch: $(cat "$SBX/run/autooff.json")"
|
||||
[ "$(autooff_field "$SBX/run/autooff.json" deferrals)" = "0" ] && pass "Verschiebungszaehler zurueckgesetzt" || fail "deferrals nicht zurueckgesetzt: $(cat "$SBX/run/autooff.json")"
|
||||
STATUS=$(run_cli "$SBX" status 2>&1)
|
||||
echo "$STATUS" | grep -qE "Auto-Aus: in (29|30) min" && pass "die Restzeit steht wieder bei 30 min" || fail "Restzeit nicht zurueckgestellt: $STATUS"
|
||||
echo "$STATUS" | grep -qE "Auto-off: in (29|30) min" && pass "die Restzeit steht wieder bei 30 min" || fail "Restzeit nicht zurueckgestellt: $STATUS"
|
||||
grep -q -- "--on-active=1800s" "$SBX/cmd.log" && pass "der Timer wurde neu gestellt" || fail "kein neuer Timer: $(cat "$SBX/cmd.log")"
|
||||
# Mit Argument gilt das Argument - und ungueltige Angaben werden auch hier abgelehnt.
|
||||
set +e
|
||||
@@ -1262,7 +1262,7 @@ RC=$?
|
||||
set -e
|
||||
ERR=$(cat "$SBX/stderr.log")
|
||||
[ "$RC" -ne 0 ] && pass "Exit-Code != 0" || fail "Exit-Code 0, obwohl der Timer nicht angelegt werden konnte: $OUT"
|
||||
echo "$ERR" | grep -qi "fehler" && pass "Fehlermeldung auf stderr" || fail "keine Fehlermeldung: $ERR"
|
||||
echo "$ERR" | grep -qi "error" && pass "Fehlermeldung auf stderr" || fail "keine Fehlermeldung: $ERR"
|
||||
[ ! -e "$SBX/run/autooff.json" ] && pass "keine Zustandsdatei, die einen Zeitschalter vortaeuscht" || fail "autooff.json trotz gescheitertem Timer: $(cat "$SBX/run/autooff.json")"
|
||||
echo "$ERR" | grep -q "off" && pass "die Meldung sagt, dass die Station AN bleibt" || fail "die Meldung verschweigt den Zustand der Station: $ERR"
|
||||
|
||||
@@ -1294,7 +1294,7 @@ set -e
|
||||
MODE=$(file_mode "$SBX/var/imageset")
|
||||
[ "$MODE" = "644" ] && pass "Rechte 0644 (der Anzeigedienst muss lesen koennen)" || fail "Rechte $MODE statt 644"
|
||||
[ -z "$(find "$SBX/var" -name '.imageset-*' 2>/dev/null)" ] && pass "keine Temp-Datei liegengeblieben" || fail "Temp-Datei liegt noch da"
|
||||
echo "$OUT" | grep -qi "achtung" && pass "der Wechsel zu 'mit-wlan' warnt" || fail "keine Warnung beim Wechsel zu mit-wlan: $OUT"
|
||||
echo "$OUT" | grep -qi "caution" && pass "der Wechsel zu 'mit-wlan' warnt" || fail "keine Warnung beim Wechsel zu mit-wlan: $OUT"
|
||||
# ... und er BLEIBT stehen: ein zweiter, unabhaengiger Aufruf sieht ihn.
|
||||
OUT2=$(run_cli "$SBX" bildsatz 2>&1)
|
||||
echo "$OUT2" | grep -q "mit-wlan" && pass "der Zustand bleibt ueber Aufrufe hinweg" || fail "Zustand nicht gespeichert: $OUT2"
|
||||
@@ -1313,7 +1313,7 @@ RC=$?
|
||||
set -e
|
||||
[ "$RC" -ne 0 ] && pass "Exit-Code != 0" || fail "Exit-Code 0 fuer einen unbekannten Bildsatz"
|
||||
[ "$(cat "$SBX/var/imageset" | tr -d '\n')" = "mit-wlan" ] && pass "der bestehende Zustand blieb unveraendert" || fail "der Zustand wurde veraendert: $(cat "$SBX/var/imageset")"
|
||||
echo "$ERR" | grep -qi "fehler" && pass "Fehlermeldung auf stderr" || fail "keine Fehlermeldung: $ERR"
|
||||
echo "$ERR" | grep -qi "error" && pass "Fehlermeldung auf stderr" || fail "keine Fehlermeldung: $ERR"
|
||||
|
||||
echo
|
||||
echo "=== Bildsatz D: ein kaputter Dateiinhalt faellt auf die SICHERE Vorgabe zurueck ==="
|
||||
@@ -1326,13 +1326,13 @@ echo "$OUT" | grep -q "ohne-wlan" && pass "kaputter Inhalt -> 'ohne-wlan'" || fa
|
||||
echo "$OUT" | grep -q "migrate-nocreds" && pass "und das Verzeichnis des sicheren Satzes" || fail "falsches Verzeichnis: $OUT"
|
||||
# Der Bediener muss erfahren, dass sein Eintrag NICHT gewirkt hat - sonst haelt er die
|
||||
# Station fuer umgeschaltet.
|
||||
echo "$OUT" | grep -q "unbekannte Einstellung 'voellig-anderes'" \
|
||||
echo "$OUT" | grep -q "unknown setting 'voellig-anderes'" \
|
||||
&& pass "die unbekannte Einstellung wird beim Namen genannt" \
|
||||
|| fail "die unbekannte Einstellung wird verschwiegen: $OUT"
|
||||
# Gegenprobe: bei einem GUELTIGEN Eintrag darf diese Zeile nicht erscheinen.
|
||||
run_cli "$SBX" bildsatz mit-wlan >/dev/null 2>&1
|
||||
OUT=$(run_cli "$SBX" bildsatz 2>&1)
|
||||
echo "$OUT" | grep -q "unbekannte Einstellung" \
|
||||
echo "$OUT" | grep -q "unknown setting" \
|
||||
&& fail "meldet 'unbekannte Einstellung' fuer einen gueltigen Eintrag: $OUT" \
|
||||
|| pass "kein Fehlalarm bei gueltigem Eintrag"
|
||||
|
||||
@@ -1346,9 +1346,9 @@ ERR=$(run_cli "$SBX" bildsatz mit-wlan 2>&1 >/dev/null)
|
||||
RC=$?
|
||||
set -e
|
||||
[ "$RC" -eq 0 ] && pass "Umschalten gelingt trotzdem (der Zustand ist gueltig, nur die Bestueckung fehlt)" || fail "Exit-Code $RC"
|
||||
echo "$ERR" | grep -qi "warnung" && pass "Warnung beim Umschalten" || fail "keine Warnung: $ERR"
|
||||
echo "$ERR" | grep -qi "warning" && pass "Warnung beim Umschalten" || fail "keine Warnung: $ERR"
|
||||
OUT=$(run_cli "$SBX" status 2>&1)
|
||||
echo "$OUT" | grep -q "FEHLT" && pass "'status' sagt, dass nichts ausgeliefert wird" || fail "'status' verschweigt den fehlenden Satz: $OUT"
|
||||
echo "$OUT" | grep -q "MISSING" && pass "'status' sagt, dass nichts ausgeliefert wird" || fail "'status' verschweigt den fehlenden Satz: $OUT"
|
||||
|
||||
echo
|
||||
echo "=== Bildsatz F: ein LEERES Verzeichnis zaehlt genauso als fehlend ==="
|
||||
@@ -1356,7 +1356,7 @@ SBX="$TMPROOT/bs6"
|
||||
setup_sandbox "$SBX"
|
||||
rm -f "$SBX/images/migrate-nocreds"/*.img
|
||||
OUT=$(run_cli "$SBX" status 2>&1)
|
||||
echo "$OUT" | grep -q "FEHLT" && pass "ein leeres Bildsatzverzeichnis wird als fehlend gemeldet" || fail "ein leerer Satz gilt als vorhanden: $OUT"
|
||||
echo "$OUT" | grep -q "MISSING" && pass "ein leeres Bildsatzverzeichnis wird als fehlend gemeldet" || fail "ein leerer Satz gilt als vorhanden: $OUT"
|
||||
|
||||
echo
|
||||
echo "=== Bildsatz G: 'migrate on' nennt den geltenden Bildsatz in der Einschaltmeldung ==="
|
||||
@@ -1369,7 +1369,7 @@ OUT=$(run_cli "$SBX" migrate on --listen 2>&1)
|
||||
RC=$?
|
||||
set -e
|
||||
[ "$RC" -eq 0 ] && pass "Exit-Code 0" || fail "Exit-Code $RC: $OUT"
|
||||
echo "$OUT" | grep -q "Bildsatz: mit-wlan" && pass "die Einschaltmeldung nennt den Bildsatz" || fail "Bildsatz fehlt in der Einschaltmeldung: $OUT"
|
||||
echo "$OUT" | grep -q "Image set: mit-wlan" && pass "die Einschaltmeldung nennt den Bildsatz" || fail "Bildsatz fehlt in der Einschaltmeldung: $OUT"
|
||||
|
||||
echo
|
||||
echo "=== Bildsatz H: die Warnung 'keine Migrationsimages' meint den GEWAEHLTEN Satz ==="
|
||||
@@ -1379,12 +1379,12 @@ rm -f "$SBX/images/migrate-wlan"/*.img # nur der andere Satz ist leer
|
||||
set +e
|
||||
ERR=$(run_cli "$SBX" migrate on --listen 2>&1 >/dev/null)
|
||||
set -e
|
||||
echo "$ERR" | grep -q "keine Migrationsimages" && fail "warnt, obwohl der GEWAEHLTE Satz ('ohne-wlan') bestueckt ist: $ERR" || pass "kein Fehlalarm - gezaehlt wird im gewaehlten Satz"
|
||||
echo "$ERR" | grep -q "no migration images" && fail "warnt, obwohl der GEWAEHLTE Satz ('ohne-wlan') bestueckt ist: $ERR" || pass "kein Fehlalarm - gezaehlt wird im gewaehlten Satz"
|
||||
run_cli "$SBX" bildsatz mit-wlan >/dev/null 2>&1
|
||||
set +e
|
||||
ERR=$(run_cli "$SBX" migrate on --listen 2>&1 >/dev/null)
|
||||
set -e
|
||||
echo "$ERR" | grep -q "keine Migrationsimages" && pass "nach dem Umschalten warnt sie" || fail "keine Warnung, obwohl der gewaehlte Satz leer ist: $ERR"
|
||||
echo "$ERR" | grep -q "no migration images" && pass "nach dem Umschalten warnt sie" || fail "keine Warnung, obwohl der gewaehlte Satz leer ist: $ERR"
|
||||
|
||||
echo
|
||||
echo "=== Abschluss A: 'display final' kennt zwei verschiedene Bilder ==="
|
||||
@@ -1466,7 +1466,7 @@ set -e
|
||||
unset FAKE_DISPLAY_ACTIVE_FOR
|
||||
[ "$RC" -eq 0 ] && pass "Exit-Code 0 (ein Anzeigefehler haelt das Herunterfahren nie auf)" || fail "Exit-Code $RC: $ERR"
|
||||
grep -q "STUB display.py" "$SBX/cmd.log" && fail "es wurde auf belegte Leitungen gezeichnet - 'GPIO busy'" || pass "nicht gezeichnet, solange der Dienst haelt"
|
||||
echo "$ERR" | grep -qi "warnung" && pass "Warnung auf stderr" || fail "keine Warnung: $ERR"
|
||||
echo "$ERR" | grep -qi "warning" && pass "Warnung auf stderr" || fail "keine Warnung: $ERR"
|
||||
# Und es wurde NICHT versucht, den Dienst von hier aus zu stoppen: das liefe mitten in
|
||||
# der Abschalt-Transaktion.
|
||||
grep -q "systemctl stop ninlight-display" "$SBX/cmd.log" && fail "'systemctl stop' mitten im Herunterfahren" || pass "kein 'systemctl stop' aus dem Abschaltschritt heraus"
|
||||
@@ -1482,7 +1482,7 @@ RC=$?
|
||||
set -e
|
||||
unset FAKE_DISPLAY_SERVICE_ACTIVE_EXIT
|
||||
[ "$RC" -eq 0 ] && pass "Exit-Code 0" || fail "Exit-Code $RC - ein Anzeigefehler wuerde das Herunterfahren rot machen: $ERR"
|
||||
echo "$ERR" | grep -qi "warnung" && pass "Warnung auf stderr" || fail "keine Warnung: $ERR"
|
||||
echo "$ERR" | grep -qi "warning" && pass "Warnung auf stderr" || fail "keine Warnung: $ERR"
|
||||
|
||||
echo
|
||||
if [ "$FAILED" -eq 0 ]; then
|
||||
|
||||
@@ -14,10 +14,40 @@ import render # noqa: E402
|
||||
|
||||
def test_off_mode_says_so_and_shows_nothing_else():
|
||||
lines = render.lines({"mode": "off"})
|
||||
assert any("Station aus" in l for l in lines)
|
||||
assert any("Station off" in l for l in lines)
|
||||
assert not any("SSID" in l for l in lines)
|
||||
|
||||
|
||||
def test_off_mode_shows_the_address_the_pi_is_reachable_at():
|
||||
"""Bei ausgeschalteter Station ist der Pi ein gewoehnlicher Rechner im Heimnetz - die
|
||||
Adresse am Panel erspart die Suche im Router, bevor man per SSH drangeht."""
|
||||
lines = render.lines({"mode": "off", "ip": "192.168.2.96"})
|
||||
assert any("IP 192.168.2.96" in l for l in lines)
|
||||
|
||||
|
||||
def test_off_mode_says_when_there_is_no_network():
|
||||
"""Leeres Feld heisst "kein Netz" und wird ausdruecklich gesagt: das erklaert, warum
|
||||
SSH gerade scheitert. Eine fehlende Zeile wuerde die Frage offen lassen."""
|
||||
lines = render.lines({"mode": "off", "ip": ""})
|
||||
assert any("no network" in l for l in lines)
|
||||
assert not any("IP " in l for l in lines)
|
||||
|
||||
|
||||
def test_off_mode_without_an_address_field_claims_nothing():
|
||||
"""Aeltere display.py-Fassung (oder ein von Hand gebauter Zustand): ohne das Feld
|
||||
bleibt die Zeile weg, statt eine Adresse zu erfinden."""
|
||||
lines = render.lines({"mode": "off"})
|
||||
assert not any("IP " in l or "no network" in l for l in lines)
|
||||
|
||||
|
||||
def test_the_address_line_belongs_to_the_off_screen_only():
|
||||
"""Im Betrieb traegt wlan0 die Zugangspunkt-Adresse, die ohnehin auf jedem Statusbild
|
||||
steht - eine zweite Adresse waere irrefuehrend und kostet die knappe Zeile."""
|
||||
for mode in ("migration", "recovery"):
|
||||
lines = render.lines({"mode": mode, "phase": "auto", "ip": "192.168.2.96"})
|
||||
assert not any("192.168.2.96" in l for l in lines)
|
||||
|
||||
|
||||
def test_migration_shows_detected_lamp():
|
||||
lines = render.lines({
|
||||
"mode": "migration", "phase": "auto", "served": 3,
|
||||
@@ -57,7 +87,7 @@ def test_recovery_mode_without_traffic():
|
||||
lines = render.lines({"mode": "recovery", "ssid": "ninlight-recovery",
|
||||
"served": 0, "recent": []})
|
||||
text = "\n".join(lines)
|
||||
assert "RETTUNG" in text
|
||||
assert "RECOVERY" in text
|
||||
assert "ninlight-recovery" in text
|
||||
|
||||
|
||||
@@ -151,7 +181,7 @@ def _migration_state():
|
||||
|
||||
|
||||
def test_off_screen_is_drawn_big_and_centered():
|
||||
""""Station aus" hat fast keinen Inhalt und muss aus zwei Metern lesbar sein.
|
||||
""""Station off" hat fast keinen Inhalt und muss aus zwei Metern lesbar sein.
|
||||
|
||||
Gemessen mit echtem Pillow: mit der Schriftsuche ist die Tinte 19 px hoch und links
|
||||
wie rechts 56 px vom Rand entfernt; faellt compose() auf load_default() zurueck, sind
|
||||
@@ -519,7 +549,7 @@ def test_font_cache_keeps_different_modules_and_search_paths_apart(monkeypatch):
|
||||
|
||||
|
||||
def test_compose_loads_only_the_roles_it_actually_draws(monkeypatch):
|
||||
""""Station aus" zeichnet genau eine Rolle - die anderen vier Schriften duerfen gar
|
||||
""""Station off" zeichnet genau eine Rolle - die anderen vier Schriften duerfen gar
|
||||
nicht erst gebaut werden (faules Laden, Befund M4)."""
|
||||
real_load_font = render._load_font
|
||||
requested = []
|
||||
@@ -663,7 +693,7 @@ def test_missing_fields_do_not_raise():
|
||||
|
||||
# --- "armed": WELCHE Lampe ist freigegeben? (dritte Hardware-Abnahme, Befund I2) -----
|
||||
#
|
||||
# Frueher stand "FREIGEGEBEN: <ziel>" als Phase im Kopfbalken. Neben dem Modus bleiben
|
||||
# Frueher stand "ARMED: <target>" als Phase im Kopfbalken. Neben dem Modus bleiben
|
||||
# dort 112 px (DejaVu) - der Text misst 182 px, gezeichnet wurde "FREIGEGEBE…", also ein
|
||||
# Wortfragment, das bei jedem Ziel gleich aussieht. Der alte Test dazu
|
||||
# (test_armed_phase_is_visible) prueft lines(), also den UNGEKUERZTEN Text, und war
|
||||
@@ -760,11 +790,11 @@ def test_armed_phase_label_is_visible():
|
||||
|
||||
def test_autooff_line_appears_only_with_a_running_timer():
|
||||
without = render.lines(_migration_state())
|
||||
assert not any("Auto-Aus" in line for line in without), without
|
||||
assert not any("Auto-off" in line for line in without), without
|
||||
|
||||
state = dict(_migration_state(), autooff_minutes=7)
|
||||
with_timer = render.lines(state)
|
||||
assert "Auto-Aus in 7 min" in with_timer, with_timer
|
||||
assert "Auto-off in 7 min" in with_timer, with_timer
|
||||
|
||||
|
||||
def test_autooff_line_also_appears_in_recovery_mode():
|
||||
@@ -772,7 +802,7 @@ def test_autooff_line_also_appears_in_recovery_mode():
|
||||
die SSH-Sitzung genauso ab."""
|
||||
lines = render.lines({"mode": "recovery", "ssid": "ninlight-recovery", "served": 0,
|
||||
"recent": [], "autooff_minutes": 42})
|
||||
assert "Auto-Aus in 42 min" in lines, lines
|
||||
assert "Auto-off in 42 min" in lines, lines
|
||||
|
||||
|
||||
def test_autooff_line_is_actually_drawn_on_the_panel():
|
||||
@@ -781,7 +811,7 @@ def test_autooff_line_is_actually_drawn_on_the_panel():
|
||||
unteren Rand ab). Geprueft per Pixelvergleich gegen eine unabhaengig gezeichnete
|
||||
Referenzzeile - schriftunabhaengig und ohne behauptete Pixelbreiten."""
|
||||
state = dict(_armed_state(), autooff_minutes=7)
|
||||
expected = "Auto-Aus in 7 min"
|
||||
expected = "Auto-off in 7 min"
|
||||
# Die wievielte BEIWERKZEILE ist es? (Kopfbalken, Phase im Kopf und grosse Zeile
|
||||
# sind eigene Rollen und belegen keine Beiwerkzeile.)
|
||||
details = [text for text, role in render.blocks(state) if role == render.ROLE_DETAIL]
|
||||
@@ -800,7 +830,7 @@ def test_autooff_line_is_actually_drawn_on_the_panel():
|
||||
def test_autooff_line_says_so_when_the_deadline_has_arrived():
|
||||
lines = render.lines({"mode": "recovery", "ssid": "x", "served": 0, "recent": [],
|
||||
"autooff_minutes": 0})
|
||||
assert "Auto-Aus steht an" in lines, lines
|
||||
assert "Auto-off pending" in lines, lines
|
||||
|
||||
|
||||
def test_build_state_reads_the_autooff_file(tmp_path, monkeypatch):
|
||||
@@ -815,7 +845,7 @@ def test_build_state_reads_the_autooff_file(tmp_path, monkeypatch):
|
||||
state = display.build_state(run_dir=tmp_path, conf_path=conf_path, service_name="x")
|
||||
|
||||
assert state["autooff_minutes"] == 7
|
||||
assert "Auto-Aus in 7 min" in render.lines(state)
|
||||
assert "Auto-off in 7 min" in render.lines(state)
|
||||
|
||||
|
||||
def test_build_state_without_autooff_file_has_no_timer_field(tmp_path, monkeypatch):
|
||||
@@ -827,7 +857,7 @@ def test_build_state_without_autooff_file_has_no_timer_field(tmp_path, monkeypat
|
||||
state = display.build_state(run_dir=tmp_path, conf_path=conf_path, service_name="x")
|
||||
|
||||
assert "autooff_minutes" not in state
|
||||
assert not any("Auto-Aus" in line for line in render.lines(state))
|
||||
assert not any("Auto-off" in line for line in render.lines(state))
|
||||
|
||||
|
||||
def test_broken_autooff_file_is_treated_as_no_timer(tmp_path):
|
||||
@@ -855,7 +885,7 @@ def test_autooff_remaining_time_rounds_up(tmp_path):
|
||||
|
||||
def test_a_switched_off_station_shows_no_timer(tmp_path, monkeypatch):
|
||||
""""off" raeumt die Zustandsdatei ab - aber selbst wenn sie durch einen Absturz
|
||||
liegen bliebe, darf neben "Station aus" keine Restzeit stehen."""
|
||||
liegen bliebe, darf neben "Station off" keine Restzeit stehen."""
|
||||
monkeypatch.setattr(display, "_service_active", lambda service_name: False)
|
||||
(tmp_path / "autooff.json").write_text(json.dumps(
|
||||
{"minutes": 30, "deadline": time.time() + 600, "deferrals": 0}))
|
||||
@@ -866,7 +896,7 @@ def test_a_switched_off_station_shows_no_timer(tmp_path, monkeypatch):
|
||||
|
||||
assert state["mode"] == "off"
|
||||
assert "autooff_minutes" not in state
|
||||
assert not any("Auto-Aus" in line for line in render.lines(state))
|
||||
assert not any("Auto-off" in line for line in render.lines(state))
|
||||
|
||||
|
||||
# --- display.build_state() (Fixrunde 1, I3) ---------------------------------------
|
||||
@@ -874,7 +904,7 @@ def test_a_switched_off_station_shows_no_timer(tmp_path, monkeypatch):
|
||||
# Dieselbe Unterscheidung, die im CLI (tools/station/ninlight-station, status_cmd) zweimal
|
||||
# nachgebessert werden musste: Phase/armed_target kommen aus control.json und sind nur im
|
||||
# Migrationsmodus gueltig; ein control.json-Rest aus einer frueheren Migration darf im
|
||||
# Rettungsmodus nicht als "FREIGEGEBEN" durchschlagen.
|
||||
# Rettungsmodus nicht als "ARMED" durchschlagen.
|
||||
|
||||
def test_control_json_is_ignored_outside_migration_mode(tmp_path, monkeypatch):
|
||||
monkeypatch.setattr(display, "_service_active", lambda service_name: True)
|
||||
@@ -888,7 +918,7 @@ def test_control_json_is_ignored_outside_migration_mode(tmp_path, monkeypatch):
|
||||
assert state["mode"] == "recovery"
|
||||
assert "phase" not in state
|
||||
assert "armed_target" not in state
|
||||
assert "FREIGEGEBEN" not in "\n".join(render.lines(state))
|
||||
assert "ARMED" not in "\n".join(render.lines(state))
|
||||
|
||||
|
||||
def test_build_state_off_when_service_not_active(tmp_path, monkeypatch):
|
||||
@@ -905,7 +935,31 @@ def test_build_state_off_when_service_not_active(tmp_path, monkeypatch):
|
||||
# "off" und sonst NICHTS aus status.json - nur der Bildsatz, der auch im
|
||||
# ausgeschalteten Zustand gilt (und gerade dann gesehen werden soll).
|
||||
assert state["mode"] == "off"
|
||||
assert set(state) == {"mode", "imageset", "imageset_ok"}
|
||||
assert set(state) == {"mode", "imageset", "imageset_ok", "ip"}
|
||||
assert isinstance(state["ip"], str)
|
||||
|
||||
|
||||
def test_build_state_off_carries_the_local_address(tmp_path, monkeypatch):
|
||||
"""Die Adresse kommt aus local_ip() und landet nur im Off-Zustand."""
|
||||
monkeypatch.setattr(display, "_service_active", lambda service_name: False)
|
||||
monkeypatch.setattr(display, "local_ip", lambda: "192.168.2.96")
|
||||
state = display.build_state(run_dir=tmp_path, conf_path=tmp_path / "station.conf",
|
||||
service_name="x", imageset_path=tmp_path / "imageset")
|
||||
assert state["ip"] == "192.168.2.96"
|
||||
|
||||
|
||||
def test_build_state_running_carries_no_address(tmp_path, monkeypatch):
|
||||
monkeypatch.setattr(display, "_service_active", lambda service_name: True)
|
||||
(tmp_path / "mode").write_text("migration")
|
||||
state = display.build_state(run_dir=tmp_path, conf_path=tmp_path / "station.conf",
|
||||
service_name="x", imageset_path=tmp_path / "imageset")
|
||||
assert "ip" not in state
|
||||
|
||||
|
||||
def test_local_ip_returns_a_string_and_never_raises():
|
||||
"""Ohne Netz gibt es keine Route - dann "" statt einer Ausnahme, die das Panel
|
||||
aufhielte."""
|
||||
assert isinstance(display.local_ip(), str)
|
||||
|
||||
|
||||
def test_build_state_mode_comes_from_mode_file_not_status_json(tmp_path, monkeypatch):
|
||||
@@ -1599,7 +1653,7 @@ def test_planner_counts_an_upgraded_partial_as_a_full_refresh():
|
||||
|
||||
def test_planner_draws_fully_on_mode_change():
|
||||
planner = display.RefreshPlanner(0.0)
|
||||
planner.record_draw(0.0, ["RETTUNG"], "recovery", was_full=True)
|
||||
planner.record_draw(0.0, ["RECOVERY"], "recovery", was_full=True)
|
||||
|
||||
assert planner.decide(5.0, ["MIGRATION"], "migration") == "full"
|
||||
|
||||
@@ -1612,14 +1666,14 @@ def test_planner_draws_fully_on_mode_change():
|
||||
# wird. Geprueft wird deshalb beides: dass ein Bedienzustand das Statusbild ersetzt, und
|
||||
# dass die ECHTE Schleife ihn schnell genug aufgreift, um bedienbar zu sein.
|
||||
|
||||
_MENU_UI = {"view": "menu", "title": "MENUE", "hero": "> Station aus",
|
||||
"details": ["Anzeige auffrischen", "Herunterfahren"], "active": True}
|
||||
_MENU_UI = {"view": "menu", "title": "MENU", "hero": "> Station off",
|
||||
"details": ["Refresh display", "Shut down"], "active": True}
|
||||
|
||||
|
||||
def test_the_ui_file_lives_outside_the_stations_runtime_directory():
|
||||
"""Nicht in /run/ninlight-station: systemd raeumt dieses RuntimeDirectory beim
|
||||
regulaeren Stop der Hauptunit auf - also ausgerechnet im Zustand "Station aus", in dem
|
||||
das Menue "Rettung starten"/"Migration starten" anbietet."""
|
||||
regulaeren Stop der Hauptunit auf - also ausgerechnet im Zustand "Station off", in dem
|
||||
das Menue "Start recovery"/"Migration starten" anbietet."""
|
||||
assert str(display.UI_FILE) == "/run/ninlight-button/ui.json"
|
||||
assert not str(display.UI_FILE).startswith("/run/ninlight-station/")
|
||||
|
||||
@@ -1627,7 +1681,7 @@ def test_the_ui_file_lives_outside_the_stations_runtime_directory():
|
||||
def test_a_running_operation_replaces_the_status_screen():
|
||||
lines = render.lines({"mode": "recovery", "ssid": "ninlight-recovery", "served": 7,
|
||||
"recent": [], "ui": _MENU_UI})
|
||||
assert lines == ["MENUE", "> Station aus", "Anzeige auffrischen", "Herunterfahren"]
|
||||
assert lines == ["MENU", "> Station off", "Refresh display", "Shut down"]
|
||||
assert not any("ninlight-recovery" in l for l in lines)
|
||||
|
||||
|
||||
@@ -1652,7 +1706,7 @@ def test_a_broken_ui_file_does_not_break_the_display():
|
||||
kann - genau wie status.json darf sie die Anzeige nie zum Absturz bringen."""
|
||||
lines = render.lines({"mode": "recovery", "ui": {"view": "menu", "title": 17,
|
||||
"hero": None, "details": "nope"}})
|
||||
assert lines == ["Bedienung"]
|
||||
assert lines == ["Button menu"]
|
||||
render.compose({"mode": "recovery", "ui": {"view": "menu"}})
|
||||
|
||||
|
||||
@@ -1668,7 +1722,7 @@ def test_read_ui_ignores_a_missing_broken_or_idle_file(tmp_path):
|
||||
path.write_text(json.dumps({"view": "menu", "active": False}))
|
||||
assert display.read_ui(path) == {}
|
||||
path.write_text(json.dumps(_MENU_UI))
|
||||
assert display.read_ui(path)["hero"] == "> Station aus"
|
||||
assert display.read_ui(path)["hero"] == "> Station off"
|
||||
|
||||
|
||||
def test_the_daemon_shows_an_operation_within_half_a_second():
|
||||
@@ -1748,13 +1802,13 @@ def test_the_two_image_sets_read_differently():
|
||||
"""Die beiden Zustaende muessen sich am Panel auf einen Blick unterscheiden - eine
|
||||
Fusszeile, die nur "Bildsatz gesetzt" sagt, waere wertlos."""
|
||||
assert render.IMAGESET_LABELS["ohne-wlan"] != render.IMAGESET_LABELS["mit-wlan"]
|
||||
assert "WLAN" in render.IMAGESET_LABELS["mit-wlan"]
|
||||
assert "Wi-Fi" in render.IMAGESET_LABELS["mit-wlan"]
|
||||
|
||||
|
||||
def test_a_missing_image_set_says_so_on_the_panel():
|
||||
state = dict(_migration_state(), imageset="mit-wlan", imageset_ok=False)
|
||||
line = _imageset_line(state)
|
||||
assert line is not None and "FEHLT" in line
|
||||
assert line is not None and "MISSING" in line
|
||||
|
||||
|
||||
def test_without_an_image_set_field_nothing_is_claimed():
|
||||
@@ -1847,7 +1901,7 @@ def test_build_state_reports_a_missing_image_set(tmp_path, monkeypatch):
|
||||
state = display.build_state(run_dir=tmp_path, conf_path=conf, service_name="x",
|
||||
imageset_path=switch)
|
||||
assert state["imageset_ok"] is False
|
||||
assert "FEHLT" in _imageset_line(state)
|
||||
assert "MISSING" in _imageset_line(state)
|
||||
|
||||
# Und ein LEERES Verzeichnis zaehlt genauso als fehlend.
|
||||
(images / "migrate-wlan").mkdir()
|
||||
@@ -1884,14 +1938,14 @@ def test_the_shutdown_and_the_reboot_screen_differ():
|
||||
gekostet)."""
|
||||
off, reboot = _final_text("poweroff"), _final_text("reboot")
|
||||
assert off != reboot
|
||||
assert "AUSGESCHALTET" in off and "NEUSTART" in reboot
|
||||
assert "POWERED OFF" in off and "REBOOTING" in reboot
|
||||
# Ausschalten: erlaubt das Trennen ausdruecklich. Geprueft wird die ZUSAGE, nicht
|
||||
# der genaue Satz - der Wortlaut musste am 2026-09-15 kuerzer werden, weil die alte
|
||||
# Fassung ("Strom kann getrennt werden", 262,1 px) auf dem Panel abgeschnitten war.
|
||||
assert "trennen erlaubt" in off
|
||||
assert "Safe to unplug" in off
|
||||
# Neustart: verbietet es ebenso ausdruecklich - und sagt nirgends das Gegenteil.
|
||||
assert "NICHT trennen" in reboot
|
||||
assert "trennen erlaubt" not in reboot
|
||||
assert "Do NOT unplug" in reboot
|
||||
assert "Safe to unplug" not in reboot
|
||||
|
||||
|
||||
def test_the_reboot_screen_says_how_long_it_stays():
|
||||
@@ -1907,14 +1961,14 @@ def test_the_final_screen_carries_nothing_else():
|
||||
"imageset_ok": True, "served": 7}
|
||||
blocks = render.blocks(state)
|
||||
assert not any(role == render.ROLE_FOOTER for _, role in blocks)
|
||||
assert not any("Bildsatz" in text or "Ausgeliefert" in text for text, _ in blocks)
|
||||
assert not any("Bildsatz" in text or "Served" in text for text, _ in blocks)
|
||||
|
||||
|
||||
def test_an_unknown_final_kind_claims_neither():
|
||||
"""Lieber gar nichts behaupten als "ausgeschaltet" zu sagen, wenn gleich wieder
|
||||
gebootet wird."""
|
||||
text = " ".join(render.lines({"mode": "final", "final": "was-auch-immer"}))
|
||||
assert "AUSGESCHALTET" not in text
|
||||
assert "POWERED OFF" not in text
|
||||
assert "getrennt" not in text
|
||||
|
||||
|
||||
@@ -1965,7 +2019,7 @@ def test_display_main_passes_the_final_kind_through(monkeypatch):
|
||||
"""Die Verdrahtung in display.main() selbst: "--final reboot" muss beim Neustartbild
|
||||
landen. Ohne diese Zusage liesse sich die Spielart in main() durch eine feste
|
||||
ersetzen, ohne dass ein Test rot wuerde - und auf dem Panel stuende beim Neustart
|
||||
"Strom kann getrennt werden"."""
|
||||
"Safe to unplug"."""
|
||||
drawn = []
|
||||
|
||||
class _Panel:
|
||||
|
||||
Reference in New Issue
Block a user