- Module backend/wifi.py : scan nmcli, connexion, mots de passe persistés dans data/wifi_passwords.json
- Gère la locale FR (oui/non), déduplique les AP multiples, prend le meilleur signal
- Onglet WiFi dans le menu admin avec statut, scan, clavier virtuel AZERTY complet
- Réseaux enregistrés avec bouton connecter/oublier et affichage temporaire du mdp
- API: /api/wifi/status, /api/wifi/scan, /api/wifi/connect, /api/wifi/saved, /api/wifi/password/{ssid}
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
204 lines
6.9 KiB
Python
204 lines
6.9 KiB
Python
"""Gestion WiFi via nmcli — scan, connexion, réseaux enregistrés."""
|
|
|
|
import json
|
|
import logging
|
|
import subprocess
|
|
from pathlib import Path
|
|
|
|
log = logging.getLogger("photobooth.wifi")
|
|
|
|
WIFI_PASSWORDS_FILE = Path(__file__).parent.parent / "data" / "wifi_passwords.json"
|
|
|
|
|
|
def _nmcli_split(line):
|
|
"""Split nmcli -t output, gère les ':' échappés par '\\'."""
|
|
parts = []
|
|
cur = []
|
|
i = 0
|
|
while i < len(line):
|
|
if line[i] == '\\' and i + 1 < len(line):
|
|
cur.append(line[i + 1])
|
|
i += 2
|
|
elif line[i] == ':':
|
|
parts.append(''.join(cur))
|
|
cur = []
|
|
i += 1
|
|
else:
|
|
cur.append(line[i])
|
|
i += 1
|
|
parts.append(''.join(cur))
|
|
return parts
|
|
|
|
|
|
def _charger_mdp() -> dict:
|
|
if WIFI_PASSWORDS_FILE.exists():
|
|
try:
|
|
return json.loads(WIFI_PASSWORDS_FILE.read_text())
|
|
except Exception:
|
|
return {}
|
|
return {}
|
|
|
|
|
|
def _sauvegarder_mdp(data: dict):
|
|
WIFI_PASSWORDS_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
WIFI_PASSWORDS_FILE.write_text(json.dumps(data, indent=2, ensure_ascii=False))
|
|
|
|
|
|
def wifi_status() -> dict:
|
|
result = {"connecte": False, "ssid": None, "signal": None, "ip": None, "interface": None}
|
|
try:
|
|
r = subprocess.run(
|
|
["nmcli", "-t", "-f", "DEVICE,TYPE,STATE,CONNECTION", "device", "status"],
|
|
capture_output=True, text=True, timeout=10,
|
|
)
|
|
for line in r.stdout.strip().split("\n"):
|
|
p = _nmcli_split(line)
|
|
if len(p) >= 4 and p[1] == "wifi" and p[2] == "connected":
|
|
result.update(connecte=True, interface=p[0], ssid=p[3])
|
|
break
|
|
|
|
if result["connecte"] and result["interface"]:
|
|
r2 = subprocess.run(
|
|
["nmcli", "-t", "-f", "IP4.ADDRESS", "device", "show", result["interface"]],
|
|
capture_output=True, text=True, timeout=5,
|
|
)
|
|
for line in r2.stdout.strip().split("\n"):
|
|
if "IP4.ADDRESS" in line:
|
|
result["ip"] = line.split(":", 1)[1].strip().split("/")[0]
|
|
break
|
|
|
|
r3 = subprocess.run(
|
|
["nmcli", "-t", "-f", "ACTIVE,SIGNAL", "dev", "wifi", "list"],
|
|
capture_output=True, text=True, timeout=5,
|
|
)
|
|
for line in r3.stdout.strip().split("\n"):
|
|
p = _nmcli_split(line)
|
|
if len(p) >= 2 and p[0] in ("yes", "oui") and p[1].isdigit():
|
|
result["signal"] = int(p[1])
|
|
break
|
|
except Exception as e:
|
|
log.warning(f"wifi_status: {e}")
|
|
return result
|
|
|
|
|
|
def wifi_scan() -> list:
|
|
saved_ssids = {n["ssid"] for n in wifi_saved_list()}
|
|
networks = []
|
|
try:
|
|
r = subprocess.run(
|
|
["nmcli", "-t", "-f", "SSID,SIGNAL,SECURITY,ACTIVE", "dev", "wifi", "list", "--rescan", "yes"],
|
|
capture_output=True, text=True, timeout=20,
|
|
)
|
|
merged = {}
|
|
for line in r.stdout.strip().split("\n"):
|
|
if not line.strip():
|
|
continue
|
|
p = _nmcli_split(line)
|
|
if len(p) < 4:
|
|
continue
|
|
ssid = p[0]
|
|
if not ssid:
|
|
continue
|
|
sig = int(p[1]) if p[1].isdigit() else 0
|
|
is_active = p[3] in ("yes", "oui")
|
|
if ssid in merged:
|
|
if sig > merged[ssid]["signal"]:
|
|
merged[ssid]["signal"] = sig
|
|
if is_active:
|
|
merged[ssid]["actif"] = True
|
|
else:
|
|
merged[ssid] = {
|
|
"ssid": ssid,
|
|
"signal": sig,
|
|
"securise": bool(p[2] and p[2] not in ("--", "")),
|
|
"securite": p[2] if p[2] not in ("--", "") else "",
|
|
"actif": is_active,
|
|
"enregistre": ssid in saved_ssids,
|
|
}
|
|
networks = list(merged.values())
|
|
networks.sort(key=lambda n: (-n["actif"], -n["signal"]))
|
|
except Exception as e:
|
|
log.warning(f"wifi_scan: {e}")
|
|
return networks
|
|
|
|
|
|
def wifi_connect(ssid: str, password: str | None = None) -> dict:
|
|
mdp = _charger_mdp()
|
|
|
|
if not password and ssid in mdp:
|
|
password = mdp[ssid]
|
|
|
|
# Réseau enregistré sans nouveau mot de passe → activer la connexion existante
|
|
if not password:
|
|
try:
|
|
r = subprocess.run(
|
|
["nmcli", "connection", "up", ssid],
|
|
capture_output=True, text=True, timeout=30,
|
|
)
|
|
if r.returncode == 0:
|
|
return {"succes": True, "message": f"Connecté à {ssid}"}
|
|
except Exception:
|
|
pass
|
|
|
|
if password:
|
|
try:
|
|
# Supprimer l'ancienne connexion pour éviter les doublons
|
|
subprocess.run(["nmcli", "connection", "delete", ssid],
|
|
capture_output=True, text=True, timeout=5)
|
|
r = subprocess.run(
|
|
["nmcli", "dev", "wifi", "connect", ssid, "password", password],
|
|
capture_output=True, text=True, timeout=30,
|
|
)
|
|
if r.returncode == 0:
|
|
mdp[ssid] = password
|
|
_sauvegarder_mdp(mdp)
|
|
return {"succes": True, "message": f"Connecté à {ssid}"}
|
|
err = r.stderr.strip() or r.stdout.strip()
|
|
if "secret" in err.lower() or "no suitable" in err.lower():
|
|
return {"succes": False, "message": "Mot de passe incorrect"}
|
|
return {"succes": False, "message": err}
|
|
except subprocess.TimeoutExpired:
|
|
return {"succes": False, "message": "Timeout — le réseau ne répond pas"}
|
|
except Exception as e:
|
|
return {"succes": False, "message": str(e)}
|
|
|
|
return {"succes": False, "message": "Mot de passe requis"}
|
|
|
|
|
|
def wifi_saved_list() -> list:
|
|
saved = []
|
|
mdp = _charger_mdp()
|
|
try:
|
|
r = subprocess.run(
|
|
["nmcli", "-t", "-f", "NAME,TYPE", "connection", "show"],
|
|
capture_output=True, text=True, timeout=5,
|
|
)
|
|
for line in r.stdout.strip().split("\n"):
|
|
p = _nmcli_split(line)
|
|
if len(p) >= 2 and p[1] == "802-11-wireless":
|
|
saved.append({"ssid": p[0], "mdp_connu": p[0] in mdp})
|
|
except Exception as e:
|
|
log.warning(f"wifi_saved_list: {e}")
|
|
return saved
|
|
|
|
|
|
def wifi_forget(ssid: str) -> dict:
|
|
mdp = _charger_mdp()
|
|
try:
|
|
r = subprocess.run(
|
|
["nmcli", "connection", "delete", ssid],
|
|
capture_output=True, text=True, timeout=10,
|
|
)
|
|
if ssid in mdp:
|
|
del mdp[ssid]
|
|
_sauvegarder_mdp(mdp)
|
|
if r.returncode == 0:
|
|
return {"succes": True}
|
|
return {"succes": False, "message": r.stderr.strip()}
|
|
except Exception as e:
|
|
return {"succes": False, "message": str(e)}
|
|
|
|
|
|
def wifi_get_password(ssid: str) -> str | None:
|
|
return _charger_mdp().get(ssid)
|