feat(station): Umschalter fuer Migrationsimages mit/ohne WLAN-Daten
Die WLAN-Zugangsdaten stecken zur Bauzeit in der Firmware, es gibt also zwei Abzuege je Ziel. Die Station haelt jetzt beide vor - images/ migrate-nocreds (aus tools/build_nocreds.sh, Lampe startet im Onboarding-AP) und images/migrate-wlan (aus build/, Lampe haengt sofort im eigenen WLAN) - und der Bediener waehlt per Umschalter, welcher ausgeliefert wird: "ninlight-station bildsatz" oder der Menueeintrag am Taster. Der Zustand liegt in /var/lib/ninlight-station/imageset, ueberlebt Neustarts und steht dauerhaft in der Fusszeile des Panels - in jedem Modus, auch bei ausgeschalteter Station: man soll vor dem Einschalten sehen, was gilt. Vorgabe und Rueckfallwert in JEDEM Zweifelsfall ist "ohne-wlan". Im Automatikmodus wird jede Klyqa-Lampe in Reichweite migriert, auch fremde; ein Abzug ohne Zugangsdaten ist dann ein harmloser Fehlgriff, einer mit Zugangsdaten traegt das eigene WLAN-Passwort in ein fremdes Geraet. Fehlt der gewaehlte Satz (Verzeichnis weg oder leer), wird NICHTS ausgeliefert - kein stilles Ausweichen auf den anderen Satz. Der Server antwortet 404 mit Status "noimageset", CLI und Panel sagen "FEHLT". server.py und die Stations-Unit muessen mit: die Auswahl der ausgelieferten Datei ist genau die Aufgabe des Servers, und ein Symlink-Tausch haette "gewaehlt, aber nicht vorhanden" von "dieses Modell fehlt" nicht mehr unterscheidbar gemacht.
This commit is contained in:
@@ -97,6 +97,14 @@ RUN_DIR = Path(os.environ.get("NINLIGHT_RUN_DIR", "/run/ninlight-station"))
|
||||
# Anzeigedienst nicht ueberdauern.
|
||||
UI_FILE = Path(os.environ.get("NINLIGHT_UI_FILE", "/run/ninlight-button/ui.json"))
|
||||
STATION_CLI = os.environ.get("NINLIGHT_STATION_BIN", "/usr/local/bin/ninlight-station")
|
||||
# Der Umschalter "Migrationsimages mit oder ohne WLAN-Zugangsdaten" - dieselbe Datei, die
|
||||
# das CLI schreibt, der Server liest und der Anzeigedienst auf dem Panel zeigt. Hier wird
|
||||
# sie NUR GELESEN (fuer die Beschriftung des Menueeintrags); umgeschaltet wird wie jede
|
||||
# andere Aktion ueber das CLI.
|
||||
IMAGESET_FILE = Path(os.environ.get("NINLIGHT_IMAGESET_FILE",
|
||||
"/var/lib/ninlight-station/imageset"))
|
||||
IMAGESET_NAMES = ("ohne-wlan", "mit-wlan")
|
||||
IMAGESET_DEFAULT = "ohne-wlan"
|
||||
STATION_SERVICE = os.environ.get("NINLIGHT_STATION_SERVICE", "ninlight-station")
|
||||
# Das Herunterfahren geht NICHT ueber das CLI (das kennt es nicht) - es ist die einzige
|
||||
# Aktion dieses Dienstes, die an systemd selbst geht.
|
||||
@@ -178,6 +186,25 @@ def action_migrate_auto() -> Action:
|
||||
"in Reichweite - auch fremde"])
|
||||
|
||||
|
||||
def action_imageset(current: str) -> Action:
|
||||
"""Der Umschalter zwischen den beiden Migrations-Bildsaetzen. Der Eintrag nennt, WOHIN
|
||||
umgeschaltet wird, nicht was gerade gilt: waehrend das Menue offen ist, ersetzt es das
|
||||
Statusbild samt Fusszeile - "Bildsatz: mit WLAN" waere dann nicht zu entscheiden.
|
||||
|
||||
Der Weg nach "mit-wlan" bekommt die deutlichere Rueckfrage, der Weg zurueck die
|
||||
uebliche: nur die eine Richtung schreibt das WLAN-Passwort des Bedieners in jede
|
||||
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")
|
||||
|
||||
|
||||
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."""
|
||||
@@ -190,6 +217,7 @@ def menu_actions(state: dict) -> list[Action]:
|
||||
running = bool(state.get("running"))
|
||||
mode = state.get("mode")
|
||||
target = state.get("target")
|
||||
imageset = state.get("imageset") or IMAGESET_DEFAULT
|
||||
|
||||
if not running:
|
||||
return [
|
||||
@@ -199,6 +227,9 @@ def menu_actions(state: dict) -> list[Action]:
|
||||
# loslaesst, landet nicht im Automatikmodus.
|
||||
action_migrate_listen(),
|
||||
action_migrate_auto(),
|
||||
# Der beste Zeitpunkt zum Umschalten ist VOR dem Einschalten - deshalb steht
|
||||
# der Eintrag auch (und gerade) hier.
|
||||
action_imageset(imageset),
|
||||
action_shutdown(),
|
||||
]
|
||||
|
||||
@@ -209,6 +240,7 @@ def menu_actions(state: dict) -> list[Action]:
|
||||
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_imageset(imageset))
|
||||
# Veraendert nichts am Betrieb - einzige Auswahl ohne Rueckfrage.
|
||||
items.append(Action("Anzeige auffrischen", ("display", "test"), confirm=False,
|
||||
done_text="Anzeige aufgefrischt"))
|
||||
@@ -268,7 +300,18 @@ def _service_active(service_name: str = STATION_SERVICE) -> bool:
|
||||
return result.returncode == 0
|
||||
|
||||
|
||||
def station_state(run_dir: Path = RUN_DIR, service_name: str = STATION_SERVICE) -> dict:
|
||||
def _read_imageset(path: Path = IMAGESET_FILE) -> str:
|
||||
"""Der eingestellte Bildsatz, oder IMAGESET_DEFAULT bei fehlender/unlesbarer Datei
|
||||
bzw. unbekanntem Inhalt - dieselbe Vorgabe wie in server.py und im CLI."""
|
||||
try:
|
||||
value = path.read_text().strip()
|
||||
except OSError:
|
||||
return IMAGESET_DEFAULT
|
||||
return value if value in IMAGESET_NAMES else IMAGESET_DEFAULT
|
||||
|
||||
|
||||
def station_state(run_dir: Path = RUN_DIR, service_name: str = STATION_SERVICE,
|
||||
imageset_path: Path = IMAGESET_FILE) -> dict:
|
||||
"""Der Zustand, von dem die Menueeintraege abhaengen. Liest dieselben Quellen wie
|
||||
display.build_state() - RUN_DIR/mode (nicht status.json's "mode"-Feld, siehe dort)
|
||||
und status.json fuer das zuletzt erkannte Ziel - dupliziert aber keine Logik des
|
||||
@@ -284,7 +327,8 @@ def station_state(run_dir: Path = RUN_DIR, service_name: str = STATION_SERVICE)
|
||||
last_seen = status.get("last_seen")
|
||||
target = last_seen.get("target") if isinstance(last_seen, dict) else None
|
||||
return {"running": running, "mode": mode,
|
||||
"target": target if isinstance(target, str) and target else None}
|
||||
"target": target if isinstance(target, str) and target else None,
|
||||
"imageset": _read_imageset(imageset_path)}
|
||||
|
||||
|
||||
# --- Bedienzustand in eine Datei schreiben ------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user