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:
@@ -26,10 +26,14 @@ import button # noqa: E402
|
||||
|
||||
# --- Zustaende, von denen die Menueeintraege abhaengen -------------------------------
|
||||
|
||||
STATE_OFF = {"running": False, "mode": "recovery", "target": None}
|
||||
STATE_RECOVERY = {"running": True, "mode": "recovery", "target": None}
|
||||
STATE_MIGRATION = {"running": True, "mode": "migration", "target": "gu10_rgb"}
|
||||
STATE_MIGRATION_BLIND = {"running": True, "mode": "migration", "target": None}
|
||||
STATE_OFF = {"running": False, "mode": "recovery", "target": None,
|
||||
"imageset": "ohne-wlan"}
|
||||
STATE_RECOVERY = {"running": True, "mode": "recovery", "target": None,
|
||||
"imageset": "ohne-wlan"}
|
||||
STATE_MIGRATION = {"running": True, "mode": "migration", "target": "gu10_rgb",
|
||||
"imageset": "ohne-wlan"}
|
||||
STATE_MIGRATION_BLIND = {"running": True, "mode": "migration", "target": None,
|
||||
"imageset": "ohne-wlan"}
|
||||
|
||||
|
||||
# --- Fake-Uhr, Taster-Doppel, Protokoll ---------------------------------------------
|
||||
@@ -164,7 +168,8 @@ def _labels(state):
|
||||
|
||||
def test_menu_off_offers_starting_and_shutdown():
|
||||
assert _labels(STATE_OFF) == ["Rettung starten", "Migration: horchen",
|
||||
"Migration: AUTOMATIK", "Herunterfahren"]
|
||||
"Migration: AUTOMATIK", "Umschalten: mit WLAN",
|
||||
"Herunterfahren"]
|
||||
|
||||
|
||||
def test_the_listening_migration_comes_before_the_automatic_one():
|
||||
@@ -175,21 +180,22 @@ def test_the_listening_migration_comes_before_the_automatic_one():
|
||||
|
||||
|
||||
def test_menu_in_recovery_offers_stopping_refreshing_and_shutdown():
|
||||
assert _labels(STATE_RECOVERY) == ["Station aus", "Anzeige auffrischen",
|
||||
"Herunterfahren"]
|
||||
assert _labels(STATE_RECOVERY) == ["Station aus", "Umschalten: mit WLAN",
|
||||
"Anzeige auffrischen", "Herunterfahren"]
|
||||
|
||||
|
||||
def test_menu_in_migration_offers_the_detected_target():
|
||||
assert _labels(STATE_MIGRATION) == ["Freigabe: gu10_rgb", "Station aus",
|
||||
"Anzeige auffrischen", "Herunterfahren"]
|
||||
"Umschalten: mit WLAN", "Anzeige auffrischen",
|
||||
"Herunterfahren"]
|
||||
|
||||
|
||||
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", "Anzeige auffrischen",
|
||||
"Herunterfahren"]
|
||||
assert _labels(STATE_MIGRATION_BLIND) == ["Station aus", "Umschalten: mit WLAN",
|
||||
"Anzeige auffrischen", "Herunterfahren"]
|
||||
|
||||
|
||||
def test_only_the_refresh_entry_skips_the_confirmation():
|
||||
@@ -231,7 +237,8 @@ def test_a_short_press_steps_to_the_next_entry():
|
||||
|
||||
|
||||
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)], 7.0)
|
||||
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"
|
||||
|
||||
|
||||
@@ -257,7 +264,8 @@ def test_a_short_press_in_the_confirmation_cancels_and_returns_to_the_menu():
|
||||
|
||||
|
||||
def test_the_refresh_entry_runs_without_a_confirmation():
|
||||
rec = _drive([_short(1.0), _short(2.0), (3.0, 5.0)], 7.0, state=STATE_RECOVERY)
|
||||
rec = _drive([_short(1.0), _short(2.0), _short(3.0), (4.0, 6.0)], 8.0,
|
||||
state=STATE_RECOVERY)
|
||||
assert rec.argvs() == [["/usr/local/bin/ninlight-station", "display", "test"]]
|
||||
assert "confirm" not in rec.kinds()
|
||||
|
||||
@@ -342,8 +350,8 @@ def test_a_blocking_action_does_not_break_the_hold_gesture():
|
||||
|
||||
|
||||
def test_the_waiting_screen_is_written_before_the_slow_call():
|
||||
rec = _drive([_short(1.0), _short(2.0), (3.0, 5.0)], 8.0, state=STATE_RECOVERY,
|
||||
action_duration_s=4.0)
|
||||
rec = _drive([_short(1.0), _short(2.0), _short(3.0), (4.0, 6.0)], 9.0,
|
||||
state=STATE_RECOVERY, action_duration_s=4.0)
|
||||
kinds = [k for _t, k, _p in rec.events]
|
||||
run_at = kinds.index("run")
|
||||
assert rec.events[run_at - 1][2].get("view") == "busy"
|
||||
@@ -493,3 +501,53 @@ def test_an_unreadable_station_state_keeps_the_menu_closed(caplog):
|
||||
ui.update(0.0, True)
|
||||
assert ui.update(0.2, False) is None
|
||||
assert ui.view(0.3) == {"view": "idle", "active": False}
|
||||
|
||||
|
||||
# --- Bildsatz-Umschalter im Menue ----------------------------------------------------
|
||||
|
||||
|
||||
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)
|
||||
ohne = dict(STATE_OFF, imageset="mit-wlan")
|
||||
assert "Umschalten: ohne WLAN" in _labels(ohne)
|
||||
assert "Umschalten: mit WLAN" not in _labels(ohne)
|
||||
|
||||
|
||||
def test_the_image_set_entry_switches_through_the_cli():
|
||||
actions = button.CliActions(cli="/usr/local/bin/ninlight-station")
|
||||
assert actions.argv_for(button.action_imageset("ohne-wlan")) == [
|
||||
"/usr/local/bin/ninlight-station", "bildsatz", "mit-wlan"]
|
||||
assert actions.argv_for(button.action_imageset("mit-wlan")) == [
|
||||
"/usr/local/bin/ninlight-station", "bildsatz", "ohne-wlan"]
|
||||
|
||||
|
||||
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 back.confirm_details is None
|
||||
|
||||
|
||||
def test_the_image_set_entry_really_runs_the_switch(tmp_path):
|
||||
"""Durch die echte Hauptschleife, nicht nur ueber menu_actions(): vierter Eintrag im
|
||||
ausgeschalteten Zustand, Auswahl halten, Rueckfrage halten."""
|
||||
rec = _drive([_short(1.0), _short(2.0), _short(3.0), _short(4.0),
|
||||
(5.0, 7.0), (8.0, 10.0)], 12.0)
|
||||
assert rec.argvs() == [["/usr/local/bin/ninlight-station", "bildsatz", "mit-wlan"]]
|
||||
|
||||
|
||||
def test_station_state_reads_the_same_switch_file_as_the_cli(tmp_path, monkeypatch):
|
||||
monkeypatch.setattr(button, "_service_active", lambda service_name: False)
|
||||
switch = tmp_path / "imageset"
|
||||
state = lambda: button.station_state(run_dir=tmp_path, service_name="x",
|
||||
imageset_path=switch)
|
||||
assert state()["imageset"] == "ohne-wlan" # Datei fehlt -> sichere Vorgabe
|
||||
switch.write_text("mit-wlan\n")
|
||||
assert state()["imageset"] == "mit-wlan"
|
||||
switch.write_text("quatsch")
|
||||
assert state()["imageset"] == "ohne-wlan"
|
||||
|
||||
Reference in New Issue
Block a user