Fixrunde 1 zu Task 6a: epd2in13_V2.init() braucht ein Pflichtargument und dessen displayPartial() setzt selbst keine LUT (neue Panel-Klasse in display.py kapselt das statt es zu verstreuen); epdconfig.module_init() war nicht wiedereintrittsfaehig und ein zu breites except tarnte GPIOPinInUse als "Panel fehlt" (jetzt idempotent, gezielter except); ninlight-display.service hatte ReadOnlyPaths=/run/ninlight-station (verhindert den Start, wenn die Station regulaer aus ist), StartLimit* in der falschen Sektion und SupplementaryGroups=spi gpio (harter Fehler auf reinem Debian ohne diese Gruppen); ein fehlendes Pillow fuehrte zu ModuleNotFoundError statt zum vorgesehenen stillen Exit 0. Dazu: Panel schlaeft nach jedem Zeichnen (Alterung), Modus-Vorrang liegt jetzt bei RUN_DIR/mode statt bei status.json, export-Praefix in der Konfigurationsdatei wird erkannt, MAX_COLS auf das mit Pillow nachgemessene Mass korrigiert, und build_state() hat jetzt eigene Tests.
130 lines
5.6 KiB
Python
130 lines
5.6 KiB
Python
import json
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "display"))
|
|
|
|
import display # noqa: E402
|
|
import render # noqa: E402
|
|
|
|
|
|
def test_off_mode_says_so_and_shows_nothing_else():
|
|
lines = render.lines({"mode": "off"})
|
|
assert any("Station aus" in l for l in lines)
|
|
assert not any("SSID" in l for l in lines)
|
|
|
|
|
|
def test_migration_shows_detected_lamp():
|
|
lines = render.lines({
|
|
"mode": "migration", "phase": "auto", "served": 3,
|
|
"ssid": "qconnex-firmware",
|
|
"last_seen": {"product_name": "Klyqa GU10 RGB CW/WW", "target": "gu10_rgb",
|
|
"variant": "prod", "lamp_version": "V1.1.0.1138"},
|
|
"recent": [{"target": "gu10_rgb", "status": "ok", "ts": "2026-09-13T13:04:02Z"}]})
|
|
text = "\n".join(lines)
|
|
assert "MIGRATION" in text
|
|
assert "Klyqa GU10 RGB CW/WW" in text
|
|
assert "V1.1.0.1138" in text
|
|
assert "prod" in text
|
|
assert "13:04" in text
|
|
|
|
|
|
def test_recovery_mode_without_traffic():
|
|
lines = render.lines({"mode": "recovery", "ssid": "ninlight-recovery",
|
|
"served": 0, "recent": []})
|
|
text = "\n".join(lines)
|
|
assert "RETTUNG" in text
|
|
assert "ninlight-recovery" in text
|
|
|
|
|
|
def test_long_product_name_is_truncated_not_wrapped():
|
|
"""Das Panel ist 250 px breit - eine zu lange Zeile darf das Layout nicht sprengen."""
|
|
lines = render.lines({"mode": "migration", "phase": "listen", "served": 0, "recent": [],
|
|
"ssid": "qconnex-firmware",
|
|
"last_seen": {"product_name": "X" * 90, "target": "e27_rgb",
|
|
"variant": "dev", "lamp_version": "V1"}})
|
|
assert all(len(l) <= render.MAX_COLS for l in lines)
|
|
|
|
|
|
def test_missing_fields_do_not_raise():
|
|
"""status.json kann jederzeit halb geschrieben oder aus einer aelteren Version sein."""
|
|
for state in ({}, {"mode": "migration"}, {"mode": "migration", "last_seen": {}}):
|
|
assert render.lines(state)
|
|
|
|
|
|
def test_armed_phase_is_visible():
|
|
lines = render.lines({"mode": "migration", "phase": "armed", "armed_target": "g95_cw",
|
|
"served": 0, "recent": [], "ssid": "qconnex-firmware"})
|
|
assert "FREIGEGEBEN" in "\n".join(lines)
|
|
|
|
|
|
# --- display.build_state() (Fixrunde 1, I3) ---------------------------------------
|
|
#
|
|
# Dieselbe Unterscheidung, die im CLI (tools/station/ninlight-station, status_cmd) zweimal
|
|
# nachgebessert werden musste: Phase/armed_target kommen aus control.json und sind nur im
|
|
# Migrationsmodus gueltig; ein control.json-Rest aus einer frueheren Migration darf im
|
|
# Rettungsmodus nicht als "FREIGEGEBEN" durchschlagen.
|
|
|
|
def test_control_json_is_ignored_outside_migration_mode(tmp_path, monkeypatch):
|
|
monkeypatch.setattr(display, "_service_active", lambda service_name: True)
|
|
(tmp_path / "mode").write_text("recovery")
|
|
(tmp_path / "control.json").write_text(json.dumps({"phase": "armed", "armed_target": "g95_cw"}))
|
|
conf_path = tmp_path / "station.conf"
|
|
conf_path.write_text("SSID='ninlight-recovery'\n")
|
|
|
|
state = display.build_state(run_dir=tmp_path, conf_path=conf_path, service_name="x")
|
|
|
|
assert state["mode"] == "recovery"
|
|
assert "phase" not in state
|
|
assert "armed_target" not in state
|
|
assert "FREIGEGEBEN" not in "\n".join(render.lines(state))
|
|
|
|
|
|
def test_build_state_off_when_service_not_active(tmp_path, monkeypatch):
|
|
monkeypatch.setattr(display, "_service_active", lambda service_name: False)
|
|
# Selbst ein noch so frisches/gueltiges status.json darf "off" nicht verhindern -
|
|
# "ninlight-station off" laesst status.json bewusst unangetastet liegen.
|
|
(tmp_path / "mode").write_text("migration")
|
|
(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")
|
|
|
|
assert state == {"mode": "off"}
|
|
|
|
|
|
def test_build_state_mode_comes_from_mode_file_not_status_json(tmp_path, monkeypatch):
|
|
"""Der Modus-Vorrang liegt bei RUN_DIR/mode, nicht bei status.json's "mode"-Feld -
|
|
status.json kann veraltet sein (Fixrunde 1, Kleinigkeit: der Vorrang war zuerst
|
|
umgekehrt)."""
|
|
monkeypatch.setattr(display, "_service_active", lambda service_name: True)
|
|
(tmp_path / "mode").write_text("recovery")
|
|
(tmp_path / "status.json").write_text(json.dumps({"mode": "migration", "served": 1}))
|
|
conf_path = tmp_path / "station.conf"
|
|
conf_path.write_text("SSID='ninlight-recovery'\n")
|
|
|
|
state = display.build_state(run_dir=tmp_path, conf_path=conf_path, service_name="x")
|
|
|
|
assert state["mode"] == "recovery"
|
|
assert state["served"] == 1
|
|
|
|
|
|
def test_build_state_falls_back_to_recovery_without_mode_file(tmp_path, monkeypatch):
|
|
monkeypatch.setattr(display, "_service_active", lambda service_name: True)
|
|
state = display.build_state(run_dir=tmp_path, conf_path=tmp_path / "station.conf",
|
|
service_name="x")
|
|
assert state["mode"] == "recovery"
|
|
|
|
|
|
def test_read_conf_accepts_export_prefix(tmp_path):
|
|
"""setup_pi.sh prueft beim Nachtragen fehlender Schluessel ausdruecklich auf ein
|
|
optionales "export "-Praefix (^(export[[:space:]]+)?KEY=) - eine bereits vorhandene
|
|
Konfigurationsdatei mit "export SSID=..." muss also genauso gelesen werden koennen."""
|
|
conf_path = tmp_path / "station.conf"
|
|
conf_path.write_text("export SSID='ninlight-recovery'\nexport MIGRATE_SSID='qconnex-firmware'\n")
|
|
|
|
values = display._read_conf(conf_path)
|
|
|
|
assert values["SSID"] == "ninlight-recovery"
|
|
assert values["MIGRATE_SSID"] == "qconnex-firmware"
|