Admin WiFi : scan réseaux, connexion, mots de passe mémorisés
- 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>
This commit is contained in:
203
backend/wifi.py
Normal file
203
backend/wifi.py
Normal file
@@ -0,0 +1,203 @@
|
||||
"""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)
|
||||
Reference in New Issue
Block a user