feat(station): Panel und CLI auf Englisch, IP-Zeile im Off-Bild
Die deutschen Texte sahen auf dem 2,13"-Panel merkwuerdig aus - jetzt sind alle Texte, die der Bediener am Geraet oder auf der Konsole sieht, englisch: Modus-, Phasen-, Bildsatz- und Abschlussbeschriftungen in render.py, die Bedientexte des Tasterdienstes und saemtliche Ausgaben von ninlight-station inklusive des Hilfetexts. Die Testbeschreibungen der Suiten bleiben deutsch - die liest nur, wer die Tests laufen laesst. Neu im Off-Bild: die Adresse, unter der der Pi gerade erreichbar ist. Ist die Station aus, ist der Pi ein gewoehnlicher Rechner im Heimnetz, und die einzige Auskunft, die man dann am Panel braucht, ist die fuer SSH. Die Adresse kommt aus einem UDP-Socket, der nur verbunden und nie benutzt wird (kein Datenverkehr, kein DNS, kein Unterprozess in einer 5-s-Schleife) und liefert genau die Adresse der Standardroute. Ohne Netz sagt die Zeile ausdruecklich "no network": eine fehlende Zeile wuerde offen lassen, warum SSH gerade scheitert. Die Zeile gibt es NUR im Off-Bild - im Betrieb traegt wlan0 die Adresse des Zugangspunkts, die ohnehin auf jedem Statusbild steht.
This commit is contained in:
@@ -167,43 +167,43 @@ def _labels(state):
|
||||
|
||||
|
||||
def test_menu_off_offers_starting_and_shutdown():
|
||||
assert _labels(STATE_OFF) == ["Rettung starten", "Migration: horchen",
|
||||
"Migration: AUTOMATIK", "Umschalten: mit WLAN",
|
||||
"Herunterfahren"]
|
||||
assert _labels(STATE_OFF) == ["Start recovery", "Migration: listen",
|
||||
"Migration: AUTOMATIC", "Switch to: with Wi-Fi",
|
||||
"Shut down"]
|
||||
|
||||
|
||||
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")
|
||||
assert labels.index("Migration: listen") < labels.index("Migration: AUTOMATIC")
|
||||
|
||||
|
||||
def test_menu_in_recovery_offers_stopping_refreshing_and_shutdown():
|
||||
assert _labels(STATE_RECOVERY) == ["Station aus", "Umschalten: mit WLAN",
|
||||
"Anzeige auffrischen", "Herunterfahren"]
|
||||
assert _labels(STATE_RECOVERY) == ["Station off", "Switch to: with Wi-Fi",
|
||||
"Refresh display", "Shut down"]
|
||||
|
||||
|
||||
def test_menu_in_migration_offers_the_detected_target():
|
||||
assert _labels(STATE_MIGRATION) == ["Freigabe: gu10_rgb", "Station aus",
|
||||
"Umschalten: mit WLAN", "Anzeige auffrischen",
|
||||
"Herunterfahren"]
|
||||
assert _labels(STATE_MIGRATION) == ["Arm: gu10_rgb", "Station off",
|
||||
"Switch to: with Wi-Fi", "Refresh display",
|
||||
"Shut down"]
|
||||
|
||||
|
||||
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", "Umschalten: mit WLAN",
|
||||
"Anzeige auffrischen", "Herunterfahren"]
|
||||
assert "Armed: " not in " ".join(_labels(STATE_MIGRATION_BLIND))
|
||||
assert _labels(STATE_MIGRATION_BLIND) == ["Station off", "Switch to: with Wi-Fi",
|
||||
"Refresh display", "Shut down"]
|
||||
|
||||
|
||||
def test_only_the_refresh_entry_skips_the_confirmation():
|
||||
"""Zusage: JEDE veraendernde Auswahl bekommt eine Rueckfrage. "Anzeige auffrischen"
|
||||
"""Zusage: JEDE veraendernde Auswahl bekommt eine Rueckfrage. "Refresh display"
|
||||
veraendert nichts und braucht keine."""
|
||||
for state in (STATE_OFF, STATE_RECOVERY, STATE_MIGRATION):
|
||||
for action in button.menu_actions(state):
|
||||
assert action.confirm is (action.label != "Anzeige auffrischen"), action.label
|
||||
assert action.confirm is (action.label != "Refresh display"), action.label
|
||||
|
||||
|
||||
def test_actions_run_through_the_station_cli():
|
||||
@@ -222,7 +222,7 @@ def test_a_short_press_opens_the_menu():
|
||||
rec = _drive([_short(1.0)], 4.0)
|
||||
assert rec.argvs() == []
|
||||
t, view = rec.first_view("menu")
|
||||
assert view["hero"] == "> Rettung starten"
|
||||
assert view["hero"] == "> Start recovery"
|
||||
assert view["active"] is True
|
||||
# Spuerbar, aber bedienbar: das Menue steht binnen eines Schleifentakts nach dem
|
||||
# Loslassen (die Verzoegerung am Panel kommt aus dessen eigenem Takt, siehe
|
||||
@@ -233,19 +233,19 @@ 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: horchen"
|
||||
assert rec.views()[-1][1]["hero"] == "> Migration: listen"
|
||||
|
||||
|
||||
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),
|
||||
_short(6.0)], 8.0)
|
||||
assert rec.views()[-1][1]["hero"] == "> Rettung starten"
|
||||
assert rec.views()[-1][1]["hero"] == "> Start recovery"
|
||||
|
||||
|
||||
def test_holding_one_and_a_half_seconds_selects_the_entry():
|
||||
rec = _drive([_short(1.0), (2.0, 4.0)], 6.0)
|
||||
t, view = rec.first_view("confirm")
|
||||
assert view["hero"] == "Rettung starten"
|
||||
assert view["hero"] == "Start recovery"
|
||||
assert 3.5 <= t <= 3.6
|
||||
# Die Auswahl allein fuehrt noch nichts aus - dafuer ist die Rueckfrage da.
|
||||
assert rec.argvs() == []
|
||||
@@ -271,7 +271,7 @@ def test_the_refresh_entry_runs_without_a_confirmation():
|
||||
|
||||
|
||||
def test_the_listening_entry_calls_the_cli_with_listen():
|
||||
"""Der zweite Eintrag ("Migration: horchen") ruft das CLI MIT --listen: der
|
||||
"""Der zweite Eintrag ("Migration: listen") 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)
|
||||
@@ -280,7 +280,7 @@ def test_the_listening_entry_calls_the_cli_with_listen():
|
||||
|
||||
|
||||
def test_the_automatic_entry_calls_the_cli_without_listen():
|
||||
"""Der dritte Eintrag ("Migration: AUTOMATIK") ruft dasselbe CLI OHNE --listen - das
|
||||
"""Der dritte Eintrag ("Migration: AUTOMATIC") 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)
|
||||
@@ -293,18 +293,18 @@ def test_the_automatic_entry_asks_a_louder_question_than_the_others():
|
||||
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"
|
||||
assert view["title"] == "WARNING"
|
||||
assert view["hero"] == "Migration: AUTOMATIC"
|
||||
text = " ".join(view["details"]).lower()
|
||||
assert "jede" in text and "fremde" in text
|
||||
assert "every" in text and "other people" in text
|
||||
# Die Bedienhinweise bleiben trotzdem stehen, und zwar zuletzt.
|
||||
assert view["details"][-2:] == ["halten (1,5 s): ja", "kurz: nein"]
|
||||
assert view["details"][-2:] == ["hold (1.5 s): yes", "tap: no"]
|
||||
|
||||
# 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"]
|
||||
assert plain["title"] == "CONFIRM"
|
||||
assert plain["details"] == ["hold (1.5 s): yes", "tap: no"]
|
||||
|
||||
|
||||
def test_the_cancel_fires_at_three_seconds_while_the_button_is_still_held():
|
||||
@@ -316,7 +316,7 @@ def test_the_cancel_fires_at_three_seconds_while_the_button_is_still_held():
|
||||
assert 4.0 <= t <= 4.1
|
||||
# ... und danach laeuft am Panel der Countdown der zweiten Stufe.
|
||||
later = [v for vt, v in rec.views() if vt > t]
|
||||
assert any("Herunterfahren in" in v.get("hero", "") for v in later)
|
||||
assert any("Shutting down in" in v.get("hero", "") for v in later)
|
||||
|
||||
|
||||
def test_releasing_between_three_and_ten_seconds_does_not_shut_down():
|
||||
@@ -337,8 +337,8 @@ def test_the_shutdown_countdown_is_visible_before_it_happens():
|
||||
erreichbar - der Countdown muss also vorher auf dem Panel gestanden haben."""
|
||||
rec = _drive([(1.0, 12.0)], 14.0)
|
||||
heroes = [v.get("hero", "") for t, v in rec.views() if t < 11.0]
|
||||
assert "Herunterfahren in 6 s" in heroes
|
||||
assert "Herunterfahren in 1 s" in heroes
|
||||
assert "Shutting down in 6 s" in heroes
|
||||
assert "Shutting down in 1 s" in heroes
|
||||
|
||||
|
||||
def test_a_blocking_action_does_not_break_the_hold_gesture():
|
||||
@@ -392,7 +392,7 @@ def test_an_expired_confirmation_leaves_nothing_pending():
|
||||
|
||||
def test_a_failed_action_says_so_on_the_panel():
|
||||
rec = _drive([_short(1.0), (2.0, 4.0), (5.0, 7.0)], 9.0, rc=1)
|
||||
assert any(v.get("view") == "result" and v.get("title") == "FEHLER"
|
||||
assert any(v.get("view") == "result" and v.get("title") == "ERROR"
|
||||
for _t, v in rec.views())
|
||||
|
||||
|
||||
@@ -411,7 +411,7 @@ def test_the_ui_file_is_written_atomically_with_0644(tmp_path):
|
||||
geschrieben."""
|
||||
path = tmp_path / "run" / "ui.json"
|
||||
writer = button.UiWriter(path)
|
||||
view = {"view": "menu", "title": "MENUE", "hero": "> Station aus", "details": [],
|
||||
view = {"view": "menu", "title": "MENU", "hero": "> Station off", "details": [],
|
||||
"active": True}
|
||||
# Unter einer strengen umask geschrieben: die Rechte muessen ausdruecklich gesetzt
|
||||
# werden, nicht zufaellig aus der umask des Dienstes fallen - sonst laege die Zusage
|
||||
@@ -509,10 +509,10 @@ def test_an_unreadable_station_state_keeps_the_menu_closed(caplog):
|
||||
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)
|
||||
assert "Switch to: with Wi-Fi" 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)
|
||||
assert "Switch to: no Wi-Fi" in _labels(ohne)
|
||||
assert "Switch to: with Wi-Fi" not in _labels(ohne)
|
||||
|
||||
|
||||
def test_the_image_set_entry_switches_through_the_cli():
|
||||
@@ -527,9 +527,9 @@ 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 loud.confirm_title == "WARNING"
|
||||
assert back.confirm_title == "CONFIRM"
|
||||
assert "Wi-Fi password" in " ".join(loud.confirm_details)
|
||||
assert back.confirm_details is None
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user