feat(station): optionaler Zeitschalter schaltet die Station selbst ab

"on"/"recovery on"/"migrate on" nehmen jetzt "--for <minuten>" (1-240): die
Station schaltet sich danach selbsttaetig ab und kehrt ins Heim-WLAN zurueck.
Das hilft, wenn man ueber WLAN angemeldet ist - "on" macht wlan0 zum
Zugangspunkt und reisst jede SSH-Sitzung ab.

Ausdruecklich optional: ohne "--for" entsteht kein Zeitschalter und alles
verhaelt sich exakt wie bisher. Dazu "renew [<minuten>]" (Uhr zurueckstellen),
"hold" (abbestellen, Station bleibt an) und eine Restzeit in "status"; "off"
raeumt einen laufenden Zeitschalter mit ab.

Umgesetzt als transienter systemd-Timer unter festem Unit-Namen (systemd-run),
Zustand in RUN_DIR/autooff.json. Vor dem Abschalten wird per "ss" geprueft, ob
noch eine Verbindung auf Port 80 aus dem Stationsnetz offen ist - dann wird um
rund eine Minute verschoben, hoechstens zehnmal (Begruendung im Kommentar).
Das Panel zeigt den Zeitschalter als eigene Zeile, aber nur wenn einer laeuft.
This commit is contained in:
Michael Sauer
2026-09-14 19:39:42 +02:00
parent f105a0eb69
commit 1f01b29b8e
5 changed files with 841 additions and 17 deletions
+301 -1
View File
@@ -172,7 +172,29 @@ echo "STUB sudo \$*" >>"$log"
exec "\$@"
EOF
chmod +x "$bindir"/nmcli "$bindir"/systemctl "$bindir"/sudo
# Der Zeitschalter legt seinen Timer per "systemd-run --unit=..." an; der Stub
# protokolliert nur, was angefordert wurde (der ECHTE Beleg dafuer, dass ein
# Zeitschalter existiert bzw. nicht existiert, ist unten immer die Zustandsdatei
# $RUN_DIR/autooff.json und das Verhalten des Skripts, nicht diese Zeile).
cat >"$bindir/systemd-run" <<EOF
#!/usr/bin/env bash
echo "STUB systemd-run \$*" >>"$log"
exit "\${FAKE_SYSTEMD_RUN_EXIT:-0}"
EOF
# "ss" beantwortet die Frage "laeuft gerade eine Auslieferung?" - mit
# FAKE_SS_BUSY=1 gibt es eine offene Verbindung aus dem Stationsnetz auf Port 80.
cat >"$bindir/ss" <<EOF
#!/usr/bin/env bash
echo "STUB ss \$*" >>"$log"
if [ -n "\${FAKE_SS_BUSY:-}" ]; then
echo "ESTAB 0 0 192.168.8.1:80 192.168.8.42:52344"
fi
exit 0
EOF
chmod +x "$bindir"/nmcli "$bindir"/systemctl "$bindir"/sudo \
"$bindir"/systemd-run "$bindir"/ss
}
# --- Sandbox fuer einen Testfall: eigene conf/RUN_DIR/IMAGE_DIR/nmcli-Zustand ---
@@ -871,6 +893,284 @@ echo "$HELP" | grep -q "EPD_REVISION" && pass "Hilfetext nennt EPD_REVISION" ||
echo "$HELP" | grep -q "ninlight-station.conf" && pass "Hilfetext nennt die Datei, in der die Revision steht" || fail "Hilfetext nennt /etc/ninlight-station.conf nicht: $HELP"
echo "$HELP" | grep -qE "V2|V3" && pass "Hilfetext nennt die beiden moeglichen Revisionen" || fail "Hilfetext nennt V2/V3 nicht: $HELP"
echo
echo "########## Zeitschalter (Auto-Aus) ##########"
# Geprueft wird durchgehend das ERGEBNIS: die Zustandsdatei $RUN_DIR/autooff.json (sie
# ist im Skript die einzige Quelle dafuer, ob ein Zeitschalter laeuft), der
# Rueckgabewert, die Ausgabe und der nmcli-/systemd-Zustand - nicht die Interna der
# Stubs.
autooff_field() {
# $1 = Pfad zu autooff.json, $2 = Feldname
python3 -c "import json,sys; print(json.load(open(sys.argv[1])).get(sys.argv[2]))" "$1" "$2" 2>/dev/null \
|| echo "FEHLT"
}
write_autooff() {
# $1 = sandbox, $2 = minutes, $3 = deadline-Offset in Sekunden ab jetzt, $4 = deferrals
python3 - "$1/run/autooff.json" "$2" "$3" "$4" <<'PY'
import json, sys, time
path, minutes, offset, deferrals = sys.argv[1:5]
with open(path, "w") as fh:
json.dump({"minutes": int(minutes),
"deadline": int(time.time()) + int(offset),
"deferrals": int(deferrals)}, fh)
PY
}
echo "=== Fall Z1 (die wichtigste Zusage): 'on' OHNE --for legt keinen Zeitschalter an ==="
# Der Regelfall ist der Pi am Kabel - dem darf niemand den Zugangspunkt unter der
# laufenden Arbeit wegschalten, nur weil jemand vorsichtig sein wollte.
SBX="$TMPROOT/tZ1"
setup_sandbox "$SBX"
set +e
OUT=$(run_cli "$SBX" on 2>&1)
RC=$?
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"
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
echo "=== Fall Z2: 'on --for 30' legt einen Zeitschalter an ==="
SBX="$TMPROOT/tZ2"
setup_sandbox "$SBX"
set +e
OUT=$(run_cli "$SBX" on --for 30 2>&1)
RC=$?
set -e
[ "$RC" -eq 0 ] && pass "Exit-Code 0" || fail "Exit-Code $RC, Ausgabe: $OUT"
[ -e "$SBX/run/autooff.json" ] && pass "Zustandsdatei angelegt" || fail "keine autooff.json angelegt: $(cat "$SBX/cmd.log")"
[ "$(autooff_field "$SBX/run/autooff.json" minutes)" = "30" ] && pass "angeforderte Dauer 30 min gemerkt" || fail "minutes falsch: $(cat "$SBX/run/autooff.json")"
[ "$(autooff_field "$SBX/run/autooff.json" deferrals)" = "0" ] && pass "noch keine Verschiebung" || fail "deferrals falsch: $(cat "$SBX/run/autooff.json")"
[ "$(file_mode "$SBX/run/autooff.json")" = "644" ] && pass "autooff.json ist 0644 (display.py muss sie lesen)" || fail "falsche Rechte: $(file_mode "$SBX/run/autooff.json")"
grep -q "systemd-run .*--unit=ninlight-station-autooff" "$SBX/cmd.log" && pass "Timer unter festem Unit-Namen angelegt" || fail "kein systemd-run mit festem Unit-Namen: $(cat "$SBX/cmd.log")"
grep -q -- "--on-active=1800s" "$SBX/cmd.log" && pass "Timer feuert nach 30 min" || fail "falsche Laufzeit: $(grep systemd-run "$SBX/cmd.log")"
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
echo "=== Fall Z3: ungueltige Minutenangaben schalten GAR NICHTS ein ==="
# Geprueft wird nicht nur der Rueckgabewert: nach einer abgelehnten Angabe darf weder ein
# Zugangspunkt hochgefahren noch der Dienst neu gestartet noch eine Modusdatei
# geschrieben worden sein.
for BAD in "0" "241" "abc" "-5" "1.5" ""; do
SBX="$TMPROOT/tZ3-${BAD:-leer}"
setup_sandbox "$SBX"
# Logdatei anlegen, BEVOR geprueft wird (wie in Fall D7): eine fehlende Datei wuerde
# die "hat nichts eingeschaltet"-Zusagen unten sonst durch ein grep-Versagen erfuellen
# statt durch das Verhalten des Skripts.
: >"$SBX/cmd.log"
set +e
if [ -z "$BAD" ]; then
OUT=$(run_cli "$SBX" on --for 2>"$SBX/stderr.log") # --for ohne Wert
else
OUT=$(run_cli "$SBX" on --for "$BAD" 2>"$SBX/stderr.log")
fi
RC=$?
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"
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")"
[ ! -e "$SBX/run/autooff.json" ] && pass "'--for ${BAD:-<leer>}': kein Zeitschalter angelegt" || fail "'--for ${BAD:-<leer>}': Zeitschalter trotz ungueltiger Angabe"
done
echo
echo "=== Fall Z4: 'migrate on --listen --for 5' - beides zusammen ==="
SBX="$TMPROOT/tZ4"
setup_sandbox "$SBX"
echo "dummy" >"$SBX/images/migrate/gu10_rgb_prod.img"
set +e
OUT=$(run_cli "$SBX" migrate on --listen --for 5 2>&1)
RC=$?
set -e
[ "$RC" -eq 0 ] && pass "Exit-Code 0" || fail "Exit-Code $RC, Ausgabe: $OUT"
[ "$(control_phase "$SBX/run/control.json")" = "listen" ] && pass "phase=listen bleibt erhalten" || fail "phase falsch: $(cat "$SBX/run/control.json" 2>&1)"
[ "$(autooff_field "$SBX/run/autooff.json" minutes)" = "5" ] && pass "Zeitschalter ueber 5 min angelegt" || fail "Zeitschalter falsch/fehlt: $(cat "$SBX/run/autooff.json" 2>&1)"
# Umgekehrte Reihenfolge der Schalter muss genauso gehen.
SBX="$TMPROOT/tZ4b"
setup_sandbox "$SBX"
echo "dummy" >"$SBX/images/migrate/gu10_rgb_prod.img"
set +e
OUT=$(run_cli "$SBX" migrate on --for 5 --listen 2>&1)
RC=$?
set -e
[ "$RC" -eq 0 ] && [ "$(control_phase "$SBX/run/control.json")" = "listen" ] \
&& pass "Schalterreihenfolge ist egal" || fail "'--for 5 --listen' scheiterte (Exit $RC): $OUT"
echo
echo "=== Fall Z5 (nicht verhandelbar): der Zeitschalter schaltet NICHT in eine laufende Auslieferung ==="
SBX="$TMPROOT/tZ5"
setup_sandbox "$SBX"
run_cli "$SBX" on --for 30 >/dev/null 2>&1
: >"$SBX/cmd.log"
set +e
OUT=$(FAKE_SS_BUSY=1 run_cli "$SBX" __autooff 2>&1)
RC=$?
set -e
[ "$RC" -eq 0 ] && pass "Exit-Code 0" || fail "Exit-Code $RC, Ausgabe: $OUT"
grep -q "systemctl stop ninlight-station$" "$SBX/cmd.log" && fail "die Station wurde mitten in der Auslieferung abgeschaltet: $(cat "$SBX/cmd.log")" || pass "die Station bleibt an"
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"
# Gegenprobe: dieselbe Lage ohne laufende Verbindung schaltet sehr wohl ab.
SBX="$TMPROOT/tZ5b"
setup_sandbox "$SBX"
run_cli "$SBX" on --for 30 >/dev/null 2>&1
: >"$SBX/cmd.log"
set +e
OUT=$(run_cli "$SBX" __autooff 2>&1)
RC=$?
set -e
[ "$RC" -eq 0 ] && pass "Gegenprobe: Exit-Code 0" || fail "Gegenprobe: Exit-Code $RC: $OUT"
grep -q "systemctl stop ninlight-station$" "$SBX/cmd.log" && pass "ohne laufende Auslieferung schaltet der Zeitschalter ab" || fail "der Zeitschalter schaltet gar nicht ab: $(cat "$SBX/cmd.log")"
ACTIVE_AFTER=$(cat "$SBX/nmstate/active")
echo "$ACTIVE_AFTER" | grep -qx "home-wifi" && pass "wlan0 ist zurueck im Heim-WLAN" || fail "nicht zurueck im Heim-WLAN: $ACTIVE_AFTER"
[ ! -e "$SBX/run/autooff.json" ] && pass "der abgelaufene Zeitschalter ist abgeraeumt" || fail "Zustandsdatei blieb liegen"
# Die Pruefung muss die richtige Frage stellen: offene Verbindungen auf Port 80 aus dem
# Stationsnetz.
grep -q "STUB ss .*:80" "$SBX/cmd.log" && pass "geprueft wird Port 80" || fail "die Verbindungspruefung fragt nicht nach Port 80: $(grep 'STUB ss' "$SBX/cmd.log")"
grep -q "STUB ss .*192.168.8.0/24" "$SBX/cmd.log" && pass "geprueft wird das Stationsnetz 192.168.8.0/24" || fail "die Verbindungspruefung fragt nicht nach dem Stationsnetz: $(grep 'STUB ss' "$SBX/cmd.log")"
echo
echo "=== Fall Z6: die Obergrenze fuers Verschieben greift - eine haengende Verbindung haelt den Zeitschalter nicht ewig auf ==="
SBX="$TMPROOT/tZ6"
setup_sandbox "$SBX"
run_cli "$SBX" on --for 30 >/dev/null 2>&1
write_autooff "$SBX" 30 60 10 # 10 Verschiebungen sind die Obergrenze
: >"$SBX/cmd.log"
set +e
OUT=$(FAKE_SS_BUSY=1 run_cli "$SBX" __autooff 2>"$SBX/stderr.log")
RC=$?
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"
[ ! -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).
SBX="$TMPROOT/tZ6b"
setup_sandbox "$SBX"
run_cli "$SBX" on --for 30 >/dev/null 2>&1
write_autooff "$SBX" 30 60 9
: >"$SBX/cmd.log"
set +e
FAKE_SS_BUSY=1 run_cli "$SBX" __autooff >/dev/null 2>&1
set -e
grep -q "systemctl stop ninlight-station$" "$SBX/cmd.log" && fail "eine Verschiebung unter der Grenze hat schon abgeschaltet" || pass "unter der Grenze wird weiter verschoben"
[ "$(autooff_field "$SBX/run/autooff.json" deferrals)" = "10" ] && pass "Zaehler steht jetzt auf der Grenze (10)" || fail "deferrals falsch: $(cat "$SBX/run/autooff.json" 2>&1)"
echo
echo "=== Fall Z7: 'off' raeumt einen laufenden Zeitschalter mit ab ==="
# Sonst feuert er spaeter in eine Station hinein, die laengst aus ist - oder inzwischen
# wieder an.
SBX="$TMPROOT/tZ7"
setup_sandbox "$SBX"
run_cli "$SBX" on --for 30 >/dev/null 2>&1
[ -e "$SBX/run/autooff.json" ] || fail "Vorbedingung: kein Zeitschalter angelegt"
: >"$SBX/cmd.log"
set +e
OUT=$(run_cli "$SBX" off 2>&1)
RC=$?
set -e
[ "$RC" -eq 0 ] && pass "Exit-Code 0" || fail "Exit-Code $RC: $OUT"
[ ! -e "$SBX/run/autooff.json" ] && pass "Zeitschalter-Zustandsdatei ist weg" || fail "'off' liess den Zeitschalter stehen: $(cat "$SBX/run/autooff.json")"
grep -q "systemctl stop ninlight-station-autooff.timer" "$SBX/cmd.log" && pass "der Timer selbst wurde angehalten" || fail "der systemd-Timer wurde nicht angehalten: $(cat "$SBX/cmd.log")"
grep -q "systemctl stop ninlight-station-autooff$" "$SBX/cmd.log" && fail "es wurde die .service statt der .timer gestoppt - das brächte den Zeitschalter-Lauf um sich selbst" || pass "die gleichnamige .service wird nicht gestoppt"
echo
echo "=== Fall Z8: 'renew'/'hold' ohne laufenden Zeitschalter sind klare Fehler, kein stillschweigendes Neuanlegen ==="
for CMD in renew hold; do
SBX="$TMPROOT/tZ8-$CMD"
setup_sandbox "$SBX"
run_cli "$SBX" on >/dev/null 2>&1 # Station an, aber OHNE --for
: >"$SBX/cmd.log"
set +e
OUT=$(run_cli "$SBX" "$CMD" 2>"$SBX/stderr.log")
RC=$?
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"
[ ! -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
echo
echo "=== Fall Z9: 'renew' stellt die Uhr zurueck - ohne Argument auf die zuletzt angeforderte Dauer ==="
SBX="$TMPROOT/tZ9"
setup_sandbox "$SBX"
run_cli "$SBX" on --for 30 >/dev/null 2>&1
write_autooff "$SBX" 30 120 4 # Restzeit fast abgelaufen, schon viermal verschoben
: >"$SBX/cmd.log"
set +e
OUT=$(run_cli "$SBX" renew 2>&1)
RC=$?
set -e
[ "$RC" -eq 0 ] && pass "Exit-Code 0" || fail "Exit-Code $RC: $OUT"
[ "$(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"
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
OUT=$(run_cli "$SBX" renew 90 2>&1); RC=$?
set -e
[ "$RC" -eq 0 ] && [ "$(autooff_field "$SBX/run/autooff.json" minutes)" = "90" ] && pass "'renew 90' stellt auf 90 min" || fail "'renew 90' scheiterte (Exit $RC): $OUT"
set +e
OUT=$(run_cli "$SBX" renew 999 2>"$SBX/stderr.log"); RC=$?
set -e
[ "$RC" -ne 0 ] && [ "$(autooff_field "$SBX/run/autooff.json" minutes)" = "90" ] && pass "'renew 999' wird abgelehnt, der laufende Zeitschalter bleibt unveraendert" || fail "'renew 999' wurde angenommen (Exit $RC): $(cat "$SBX/run/autooff.json")"
echo
echo "=== Fall Z10: 'hold' bestellt ab - die Station bleibt an ==="
SBX="$TMPROOT/tZ10"
setup_sandbox "$SBX"
run_cli "$SBX" on --for 30 >/dev/null 2>&1
: >"$SBX/cmd.log"
set +e
OUT=$(run_cli "$SBX" hold 2>&1)
RC=$?
set -e
[ "$RC" -eq 0 ] && pass "Exit-Code 0" || fail "Exit-Code $RC: $OUT"
[ ! -e "$SBX/run/autooff.json" ] && pass "Zeitschalter abbestellt" || fail "Zeitschalter laeuft weiter: $(cat "$SBX/run/autooff.json")"
grep -q "systemctl stop ninlight-station-autooff.timer" "$SBX/cmd.log" && pass "der Timer wurde angehalten" || fail "Timer nicht angehalten: $(cat "$SBX/cmd.log")"
grep -q "systemctl stop ninlight-station$" "$SBX/cmd.log" && fail "'hold' hat die Station abgeschaltet" || pass "die Station bleibt an"
ACTIVE_AFTER=$(cat "$SBX/nmstate/active")
[ "$ACTIVE_AFTER" = "ninlight-recovery" ] && pass "der Zugangspunkt laeuft unveraendert weiter" || fail "Zugangspunkt-Zustand veraendert: $ACTIVE_AFTER"
# Und ein danach doch noch feuernder Timer-Lauf schaltet nichts ab.
: >"$SBX/cmd.log"
set +e
run_cli "$SBX" __autooff >/dev/null 2>&1
set -e
grep -q "systemctl stop ninlight-station$" "$SBX/cmd.log" && fail "ein abbestellter Zeitschalter hat trotzdem abgeschaltet: $(cat "$SBX/cmd.log")" || pass "ein abbestellter Zeitschalter schaltet nichts mehr ab"
echo
echo "=== Fall Z11: scheitert systemd-run, bleibt das nicht unbemerkt ==="
# Sonst glaubte der Nutzer an einen Zeitschalter, den es nie gab - und genau darauf
# hat er sich verlassen, als er die SSH-Sitzung ueber WLAN aufgab.
SBX="$TMPROOT/tZ11"
setup_sandbox "$SBX"
set +e
OUT=$(FAKE_SYSTEMD_RUN_EXIT=1 run_cli "$SBX" on --for 30 2>"$SBX/stderr.log")
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"
[ ! -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"
echo
if [ "$FAILED" -eq 0 ]; then
echo "Alle Tests bestanden."
+116
View File
@@ -1,5 +1,6 @@
import json
import sys
import time
from pathlib import Path
import pytest
@@ -731,6 +732,121 @@ def test_armed_phase_label_is_visible():
assert "g95_cw" in text
# --- Zeitschalter am Panel (Task 7e) ------------------------------------------------
#
# Ein Zeitschalter, den man am Panel nicht sieht, ist schlimmer als keiner: man glaubt,
# die Station laeuft, und sie ist laengst aus. Umgekehrt darf ohne Zeitschalter (der
# Regelfall - er entsteht nur mit "--for") keine Zeile dafuer erscheinen.
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
state = dict(_migration_state(), autooff_minutes=7)
with_timer = render.lines(state)
assert "Auto-Aus in 7 min" in with_timer, with_timer
def test_autooff_line_also_appears_in_recovery_mode():
"""Der Zeitschalter haengt nicht am Migrationsmodus - im Rettungsmodus reisst "on"
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
def test_autooff_line_is_actually_drawn_on_the_panel():
"""Nicht nur in lines(): die Zeile muss im vollsten Zustand auch tatsaechlich aufs
Panel kommen und nicht unten herausfallen (compose() bricht Beiwerkzeilen vor dem
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"
# 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]
assert expected in details, details
index = details.index(expected)
top, bottom = _detail_band(index)
drawn = _band_pixels(render.compose(state), top, bottom)
reference = _band_pixels(_reference_detail_line(expected, top), top, bottom)
assert drawn == reference, (
"die Zeitschalter-Zeile steht nicht (ungekuerzt) auf dem Panel - der Bediener "
"sieht nicht, dass sich die Station gleich abschaltet")
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
def test_build_state_reads_the_autooff_file(tmp_path, monkeypatch):
monkeypatch.setattr(display, "_service_active", lambda service_name: True)
(tmp_path / "mode").write_text("recovery")
conf_path = tmp_path / "station.conf"
conf_path.write_text("SSID='ninlight-recovery'\n")
# Noch 6 min 1 s - aufgerundet 7, wie es "ninlight-station status" auch rechnet.
(tmp_path / "autooff.json").write_text(json.dumps(
{"minutes": 30, "deadline": time.time() + 361, "deferrals": 0}))
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)
def test_build_state_without_autooff_file_has_no_timer_field(tmp_path, monkeypatch):
monkeypatch.setattr(display, "_service_active", lambda service_name: True)
(tmp_path / "mode").write_text("recovery")
conf_path = tmp_path / "station.conf"
conf_path.write_text("SSID='ninlight-recovery'\n")
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))
def test_broken_autooff_file_is_treated_as_no_timer(tmp_path):
"""Dieselbe Zusage wie fuer status.json/control.json: halb geschrieben, kaputt oder
aus einer aelteren Fassung darf keine Ausnahme werfen - und vor allem keine
erfundene Restzeit zeigen."""
(tmp_path / "autooff.json").write_text("{kein json")
assert display._read_autooff_minutes(tmp_path) is None
(tmp_path / "autooff.json").write_text(json.dumps({"minutes": 30}))
assert display._read_autooff_minutes(tmp_path) is None
(tmp_path / "autooff.json").write_text(json.dumps({"deadline": "gleich"}))
assert display._read_autooff_minutes(tmp_path) is None
def test_autooff_remaining_time_rounds_up(tmp_path):
(tmp_path / "autooff.json").write_text(json.dumps({"deadline": 1000.0}))
assert display._read_autooff_minutes(tmp_path, now=1000.0 - 600) == 10
assert display._read_autooff_minutes(tmp_path, now=1000.0 - 601) == 11
assert display._read_autooff_minutes(tmp_path, now=1000.0 - 1) == 1
assert display._read_autooff_minutes(tmp_path, now=1000.0) == 0
assert display._read_autooff_minutes(tmp_path, now=1000.0 + 90) == 0
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."""
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}))
state = display.build_state(run_dir=tmp_path, conf_path=tmp_path / "station.conf",
service_name="x")
assert state == {"mode": "off"}
assert not any("Auto-Aus" in line for line in render.lines(state))
# --- display.build_state() (Fixrunde 1, I3) ---------------------------------------
#
# Dieselbe Unterscheidung, die im CLI (tools/station/ninlight-station, status_cmd) zweimal