feat(station): Automatikmodus als eigener Menueeintrag am Taster

Das Tastermenue bot bisher nur "Migration starten" an, fest verdrahtet
auf die Horchphase; den Automatikmodus gab es nur ueber die
Kommandozeile - also ausgerechnet dort nicht, wo man ihn braucht, denn
"on" macht wlan0 zum Zugangspunkt und reisst jede SSH-Sitzung ab.

Jetzt zwei getrennte, namentlich unterscheidbare Eintraege:
"Migration: horchen" (--listen) zuerst, danach "Migration: AUTOMATIK"
(ohne --listen). Der Automatikeintrag bekommt eine andere, deutlichere
Rueckfrage ("ACHTUNG" statt "RUECKFRAGE", plus zwei Warnzeilen), weil
der Migrations-Zugangspunkt ein oeffentlich bekanntes Passwort hat und
im Automatikmodus jede alte Klyqa-Lampe in Funkreichweite ungefragt
migriert wird, auch fremde. Die Bedienhinweise stehen weiterhin und
zuletzt.
This commit is contained in:
Michael Sauer
2026-09-15 01:20:42 +02:00
parent 2fe2f7d9f4
commit 3642672c71
3 changed files with 115 additions and 28 deletions
+43 -9
View File
@@ -163,7 +163,15 @@ def _labels(state):
def test_menu_off_offers_starting_and_shutdown():
assert _labels(STATE_OFF) == ["Rettung starten", "Migration starten", "Herunterfahren"]
assert _labels(STATE_OFF) == ["Rettung starten", "Migration: horchen",
"Migration: AUTOMATIK", "Herunterfahren"]
def test_the_listening_migration_comes_before_the_automatic_one():
"""Der horchende Modus ist der erste und naheliegende Eintrag - wer nur blaettert und
irgendwo loslaesst, soll nicht im Automatikmodus landen."""
labels = _labels(STATE_OFF)
assert labels.index("Migration: horchen") < labels.index("Migration: AUTOMATIK")
def test_menu_in_recovery_offers_stopping_refreshing_and_shutdown():
@@ -219,11 +227,11 @@ def test_a_short_press_opens_the_menu():
def test_a_short_press_steps_to_the_next_entry():
rec = _drive([_short(1.0), _short(2.0)], 4.0)
assert rec.argvs() == []
assert rec.views()[-1][1]["hero"] == "> Migration starten"
assert rec.views()[-1][1]["hero"] == "> Migration: horchen"
def test_a_short_press_wraps_around_the_last_entry():
rec = _drive([_short(1.0), _short(2.0), _short(3.0), _short(4.0)], 6.0)
rec = _drive([_short(1.0), _short(2.0), _short(3.0), _short(4.0), _short(5.0)], 7.0)
assert rec.views()[-1][1]["hero"] == "> Rettung starten"
@@ -254,15 +262,41 @@ def test_the_refresh_entry_runs_without_a_confirmation():
assert "confirm" not in rec.kinds()
def test_starting_the_migration_always_listens():
"""Sicherheitsregel, nicht verhandelbar: der Migrations-Zugangspunkt hat ein
oeffentlich bekanntes Passwort - ein Tastendruck, der ihn im Automatikmodus
hochfaehrt, wuerde jede alte Klyqa-Lampe in Funkreichweite ungefragt migrieren, auch
fremde."""
def test_the_listening_entry_calls_the_cli_with_listen():
"""Der zweite Eintrag ("Migration: horchen") ruft das CLI MIT --listen: der
Zugangspunkt steht, es geht aber nichts raus, bis jemand eine erkannte Lampe
freigibt."""
rec = _drive([_short(1.0), _short(2.0), (3.0, 5.0), (6.0, 8.0)], 10.0)
assert rec.argvs() == [["/usr/local/bin/ninlight-station", "migrate", "on",
"--listen"]]
assert "--listen" in rec.argvs()[0]
def test_the_automatic_entry_calls_the_cli_without_listen():
"""Der dritte Eintrag ("Migration: AUTOMATIK") ruft dasselbe CLI OHNE --listen - das
ist der ganze Unterschied zwischen den beiden Eintraegen, und er entscheidet, ob
fremde Lampen ungefragt migriert werden."""
rec = _drive([_short(1.0), _short(2.0), _short(3.0), (4.0, 6.0), (7.0, 9.0)], 11.0)
assert rec.argvs() == [["/usr/local/bin/ninlight-station", "migrate", "on"]]
assert "--listen" not in rec.argvs()[0]
def test_the_automatic_entry_asks_a_louder_question_than_the_others():
"""Zusage: die Rueckfrage des Automatikmodus benennt die Folge, statt nur "Wirklich?"
zu fragen - und sie unterscheidet sich sichtbar von der der uebrigen Eintraege."""
rec = _drive([_short(1.0), _short(2.0), _short(3.0), (4.0, 6.0)], 8.0)
_t, view = rec.first_view("confirm")
assert view["title"] == "ACHTUNG"
assert view["hero"] == "Migration: AUTOMATIK"
text = " ".join(view["details"]).lower()
assert "jede" in text and "fremde" in text
# Die Bedienhinweise bleiben trotzdem stehen, und zwar zuletzt.
assert view["details"][-2:] == ["halten (1,5 s): ja", "kurz: nein"]
# Gegenprobe am horchenden Eintrag: uebliche Rueckfrage, keine Warnung.
rec = _drive([_short(1.0), _short(2.0), (3.0, 5.0)], 7.0)
_t, plain = rec.first_view("confirm")
assert plain["title"] == "RUECKFRAGE"
assert plain["details"] == ["halten (1,5 s): ja", "kurz: nein"]
def test_the_cancel_fires_at_three_seconds_while_the_button_is_still_held():