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:
@@ -113,6 +113,32 @@ def _read_conf(path: Path) -> dict[str, str]:
|
||||
return values
|
||||
|
||||
|
||||
def _read_autooff_minutes(run_dir: Path, now: float | None = None):
|
||||
"""Restzeit des optionalen Zeitschalters in ganzen Minuten (aufgerundet), oder None,
|
||||
wenn gerade keiner laeuft.
|
||||
|
||||
Quelle ist RUN_DIR/autooff.json, die "ninlight-station --for/renew" schreibt und
|
||||
"hold"/"off" wieder loescht (siehe dort). Ein Zeitschalter, den man am Panel nicht
|
||||
sieht, waere schlimmer als keiner: man glaubt, die Station laeuft, und sie ist
|
||||
laengst aus.
|
||||
|
||||
"deadline" ist eine Unix-Zeit (der Zeitschalter selbst haengt an systemds monotoner
|
||||
Uhr, die Anzeige rechnet in Wanduhrzeit) - eine Zeitverstellung verschiebt also die
|
||||
angezeigte Restzeit, nicht das tatsaechliche Abschalten. Rundet auf: solange noch
|
||||
eine Sekunde uebrig ist, steht dort "1 min" und nicht "0 min"."""
|
||||
data = _read_json(run_dir / "autooff.json")
|
||||
deadline = data.get("deadline")
|
||||
if isinstance(deadline, bool) or not isinstance(deadline, (int, float)):
|
||||
return None
|
||||
remaining_s = deadline - (time.time() if now is None else now)
|
||||
if remaining_s <= 0:
|
||||
# Der Termin ist da (oder der Lauf verschiebt gerade) - "0 min" ist hier die
|
||||
# ehrliche Auskunft, nicht das Verschwinden der Zeile.
|
||||
return 0
|
||||
whole, rest = divmod(remaining_s, 60)
|
||||
return int(whole) + (1 if rest else 0)
|
||||
|
||||
|
||||
def _read_mode_file(run_dir: Path) -> str:
|
||||
"""Modus, in dem der Dienst laut RUN_DIR/mode tatsaechlich gestartet wurde. Fehlt die
|
||||
Datei oder ist ihr Inhalt kein bekannter Modus, gilt "recovery" - derselbe Vorgabewert
|
||||
@@ -185,6 +211,12 @@ def build_state(run_dir: Path = RUN_DIR, conf_path: Path = CONF_PATH,
|
||||
state["phase"] = phase if phase in ("auto", "listen", "armed") else "auto"
|
||||
state["armed_target"] = control.get("armed_target")
|
||||
|
||||
# Der Zeitschalter ist optional und gilt in beiden Modi gleichermassen - ohne ihn
|
||||
# bleibt das Feld weg und render.blocks() zeichnet keine Zeile dafuer.
|
||||
autooff_minutes = _read_autooff_minutes(run_dir)
|
||||
if autooff_minutes is not None:
|
||||
state["autooff_minutes"] = autooff_minutes
|
||||
|
||||
return state
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user