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
|
||||
|
||||
|
||||
|
||||
@@ -281,6 +281,23 @@ def blocks(state: dict) -> list[tuple[str, str]]:
|
||||
# ohne erkannte Lampe steht die SSID ohnehin als grosse Zeile da.
|
||||
out.append((f"SSID: {ssid}", ROLE_DETAIL))
|
||||
|
||||
# Der optionale Zeitschalter, in BEIDEN Modi und vor den reinen Zaehlerzeilen: ein
|
||||
# Zeitschalter, den man am Panel nicht sieht, ist schlimmer als keiner - man glaubt,
|
||||
# die Station laeuft, und sie ist laengst aus. Fehlt er (der Regelfall, denn er
|
||||
# entsteht nur mit "--for"), fehlt die Zeile ersatzlos.
|
||||
#
|
||||
# Er steht damit VOR "Ausgeliefert"/"zuletzt ausgeliefert": im vollsten Zustand
|
||||
# passen fuenf Beiwerkzeilen aufs Panel (siehe BOTTOM_MARGIN_PX), die sechste faellt
|
||||
# unten heraus. Von diesen beiden Auskuenften ist "wann geht die Station aus"
|
||||
# diejenige, nach der man ueberhaupt aufs Panel sieht - der Zaehler der letzten
|
||||
# Auslieferung steht auch noch in "ninlight-station status".
|
||||
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))
|
||||
else:
|
||||
out.append(("Auto-Aus steht an", ROLE_DETAIL))
|
||||
|
||||
served = state.get("served")
|
||||
out.append((f"Ausgeliefert: {served if isinstance(served, int) else 0}", ROLE_DETAIL))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user