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:
@@ -861,9 +861,11 @@ def test_a_switched_off_station_shows_no_timer(tmp_path, monkeypatch):
|
||||
{"minutes": 30, "deadline": time.time() + 600, "deferrals": 0}))
|
||||
|
||||
state = display.build_state(run_dir=tmp_path, conf_path=tmp_path / "station.conf",
|
||||
service_name="x")
|
||||
service_name="x",
|
||||
imageset_path=tmp_path / "imageset")
|
||||
|
||||
assert state == {"mode": "off"}
|
||||
assert state["mode"] == "off"
|
||||
assert "autooff_minutes" not in state
|
||||
assert not any("Auto-Aus" in line for line in render.lines(state))
|
||||
|
||||
|
||||
@@ -897,9 +899,13 @@ def test_build_state_off_when_service_not_active(tmp_path, monkeypatch):
|
||||
(tmp_path / "status.json").write_text(json.dumps({"mode": "migration", "served": 5}))
|
||||
|
||||
state = display.build_state(run_dir=tmp_path, conf_path=tmp_path / "station.conf",
|
||||
service_name="x")
|
||||
service_name="x",
|
||||
imageset_path=tmp_path / "imageset")
|
||||
|
||||
assert state == {"mode": "off"}
|
||||
# "off" und sonst NICHTS aus status.json - nur der Bildsatz, der auch im
|
||||
# ausgeschalteten Zustand gilt (und gerade dann gesehen werden soll).
|
||||
assert state["mode"] == "off"
|
||||
assert set(state) == {"mode", "imageset", "imageset_ok"}
|
||||
|
||||
|
||||
def test_build_state_mode_comes_from_mode_file_not_status_json(tmp_path, monkeypatch):
|
||||
@@ -1707,3 +1713,155 @@ def test_the_expensive_station_state_is_still_only_read_every_five_seconds():
|
||||
|
||||
_drive_daemon(state_at, duration_s=10.0)
|
||||
assert calls == [0.0, 5.0, 10.0]
|
||||
|
||||
|
||||
# --- Bildsatz-Umschalter in der Fusszeile -------------------------------------------
|
||||
#
|
||||
# Zusage: welcher Migrations-Bildsatz gilt, steht DAUERHAFT auf dem Panel - nicht nur im
|
||||
# Menue. Der Zustand ueberlebt Neustarts und kann von vorletzter Woche stammen; wer ihn
|
||||
# erst beim Blaettern im Menue saehe, hat womoeglich schon zwanzig Lampen mit den eigenen
|
||||
# WLAN-Zugangsdaten geflasht.
|
||||
|
||||
def _flat(rows):
|
||||
"""_band_pixels() liefert Zeilen-Tupel - hier interessieren die einzelnen Pixel."""
|
||||
return [px for row in rows for px in row]
|
||||
|
||||
|
||||
def _imageset_line(state):
|
||||
texts = [text for text, role in render.blocks(state) if role == render.ROLE_FOOTER]
|
||||
assert len(texts) <= 1, texts
|
||||
return texts[0] if texts else None
|
||||
|
||||
|
||||
def test_the_image_set_is_shown_in_every_status_screen():
|
||||
for state in ({"mode": "off"},
|
||||
{"mode": "recovery", "ssid": "ninlight-recovery"},
|
||||
dict(_migration_state()),
|
||||
{"mode": None}):
|
||||
for name in ("ohne-wlan", "mit-wlan"):
|
||||
s = dict(state, imageset=name, imageset_ok=True)
|
||||
line = _imageset_line(s)
|
||||
assert line == render.IMAGESET_LABELS[name], (state.get("mode"), name)
|
||||
|
||||
|
||||
def test_the_two_image_sets_read_differently():
|
||||
"""Die beiden Zustaende muessen sich am Panel auf einen Blick unterscheiden - eine
|
||||
Fusszeile, die nur "Bildsatz gesetzt" sagt, waere wertlos."""
|
||||
assert render.IMAGESET_LABELS["ohne-wlan"] != render.IMAGESET_LABELS["mit-wlan"]
|
||||
assert "WLAN" in render.IMAGESET_LABELS["mit-wlan"]
|
||||
|
||||
|
||||
def test_a_missing_image_set_says_so_on_the_panel():
|
||||
state = dict(_migration_state(), imageset="mit-wlan", imageset_ok=False)
|
||||
line = _imageset_line(state)
|
||||
assert line is not None and "FEHLT" in line
|
||||
|
||||
|
||||
def test_without_an_image_set_field_nothing_is_claimed():
|
||||
"""Ein Zustand aus einer aelteren display.py-Fassung bekommt keine erfundene
|
||||
Fusszeile."""
|
||||
assert _imageset_line({"mode": "off"}) is None
|
||||
|
||||
|
||||
def test_the_image_set_line_is_actually_drawn_at_the_bottom():
|
||||
"""Nicht nur in blocks(): auf dem Panel, unterhalb aller Beiwerkzeilen und ueber dem
|
||||
unteren Rand."""
|
||||
state = dict(_migration_state(), imageset="mit-wlan", imageset_ok=True)
|
||||
footer_top = (render.PANEL_HEIGHT_PX - render.BOTTOM_MARGIN_PX
|
||||
- render.FOOTER_BAND_HEIGHT_PX)
|
||||
|
||||
with_footer = render.compose(state)
|
||||
without = render.compose({k: v for k, v in state.items()
|
||||
if k not in ("imageset", "imageset_ok")})
|
||||
|
||||
bottom = render.PANEL_HEIGHT_PX - render.BOTTOM_MARGIN_PX
|
||||
band = _band_pixels(with_footer, footer_top, bottom)
|
||||
assert 0 in _flat(band), "im Fussband ist gar keine Tinte"
|
||||
assert band != _band_pixels(without, footer_top, bottom), \
|
||||
"Fussband sieht mit und ohne Bildsatz gleich aus"
|
||||
|
||||
|
||||
def test_the_image_set_line_keeps_the_bottom_margin_free():
|
||||
state = dict(_migration_state(), imageset="mit-wlan", imageset_ok=False)
|
||||
image = render.compose(state)
|
||||
margin = _flat(_band_pixels(image, render.PANEL_HEIGHT_PX - render.BOTTOM_MARGIN_PX,
|
||||
render.PANEL_HEIGHT_PX))
|
||||
assert set(margin) <= {255}, "der untere Rand ist nicht mehr frei"
|
||||
|
||||
|
||||
def test_the_image_set_line_survives_the_fullest_state():
|
||||
"""Im vollsten Zustand faellt eine Beiwerkzeile heraus - aber nie die Fusszeile:
|
||||
sie steht an festem Platz und nicht im Zeilenfluss."""
|
||||
state = dict(_migration_state(), imageset="mit-wlan", imageset_ok=True,
|
||||
autooff_minutes=17)
|
||||
assert _imageset_line(state) is not None
|
||||
# ... und der Zeilenfluss endet wirklich vor dem Fussband.
|
||||
image = render.compose(state)
|
||||
footer_top = (render.PANEL_HEIGHT_PX - render.BOTTOM_MARGIN_PX
|
||||
- render.FOOTER_BAND_HEIGHT_PX)
|
||||
# Die Zeile direkt ueber dem Trennstrich darf ihn nicht beruehren.
|
||||
assert set(_flat(_band_pixels(image, footer_top - 1, footer_top))) <= {255}
|
||||
|
||||
|
||||
def test_build_state_reads_the_image_set_switch(tmp_path, monkeypatch):
|
||||
"""Dieselbe Datei, die das CLI schreibt und server.py liest - kein zweiter Weg."""
|
||||
monkeypatch.setattr(display, "_service_active", lambda service_name: True)
|
||||
(tmp_path / "mode").write_text("migration")
|
||||
images = tmp_path / "images"
|
||||
(images / "migrate-nocreds").mkdir(parents=True)
|
||||
(images / "migrate-nocreds" / "g95_cw_dev.img").write_bytes(b"IMG")
|
||||
(images / "migrate-wlan").mkdir()
|
||||
(images / "migrate-wlan" / "g95_cw_dev.img").write_bytes(b"IMG")
|
||||
conf = tmp_path / "station.conf"
|
||||
conf.write_text(f"MIGRATE_SSID='qconnex-firmware'\nIMAGE_DIR='{images}'\n")
|
||||
switch = tmp_path / "imageset"
|
||||
|
||||
def state():
|
||||
return display.build_state(run_dir=tmp_path, conf_path=conf, service_name="x",
|
||||
imageset_path=switch)
|
||||
|
||||
# Ohne Datei: die sichere Vorgabe.
|
||||
assert state()["imageset"] == "ohne-wlan"
|
||||
switch.write_text("mit-wlan\n")
|
||||
assert state()["imageset"] == "mit-wlan"
|
||||
# Unbekannter Inhalt darf NIE auf den teureren Satz zeigen.
|
||||
switch.write_text("irgendwas")
|
||||
assert state()["imageset"] == "ohne-wlan"
|
||||
|
||||
|
||||
def test_build_state_reports_a_missing_image_set(tmp_path, monkeypatch):
|
||||
""""Vorhanden" heisst: Verzeichnis da UND mindestens ein Image darin - dieselbe Regel
|
||||
wie in server.py, sonst sagt das Panel "in Ordnung" und die Lampe bekommt nichts."""
|
||||
monkeypatch.setattr(display, "_service_active", lambda service_name: True)
|
||||
images = tmp_path / "images"
|
||||
(images / "migrate-nocreds").mkdir(parents=True)
|
||||
(images / "migrate-nocreds" / "g95_cw_dev.img").write_bytes(b"IMG")
|
||||
conf = tmp_path / "station.conf"
|
||||
conf.write_text(f"IMAGE_DIR='{images}'\n")
|
||||
switch = tmp_path / "imageset"
|
||||
|
||||
switch.write_text("ohne-wlan")
|
||||
assert display.build_state(run_dir=tmp_path, conf_path=conf, service_name="x",
|
||||
imageset_path=switch)["imageset_ok"] is True
|
||||
switch.write_text("mit-wlan") # migrate-wlan/ gibt es nicht
|
||||
state = display.build_state(run_dir=tmp_path, conf_path=conf, service_name="x",
|
||||
imageset_path=switch)
|
||||
assert state["imageset_ok"] is False
|
||||
assert "FEHLT" in _imageset_line(state)
|
||||
|
||||
# Und ein LEERES Verzeichnis zaehlt genauso als fehlend.
|
||||
(images / "migrate-wlan").mkdir()
|
||||
assert display.build_state(run_dir=tmp_path, conf_path=conf, service_name="x",
|
||||
imageset_path=switch)["imageset_ok"] is False
|
||||
|
||||
|
||||
def test_the_display_and_the_server_agree_on_the_set_names():
|
||||
"""Zwei Prozesse, eine Wahrheit: waeren die Namen oder die Vorgabe verschieden,
|
||||
behauptete das Panel etwas anderes, als der Server tut."""
|
||||
import importlib.util
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
"server_for_names", Path(__file__).resolve().parents[1] / "server.py")
|
||||
server = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(server)
|
||||
assert display.IMAGESET_DIRS == server.IMAGE_SET_DIRS
|
||||
assert display.IMAGESET_DEFAULT == server.DEFAULT_IMAGE_SET
|
||||
|
||||
Reference in New Issue
Block a user