feat: alle FritzBoxen sichern (ueberspringt statt abzubrechen) + Tastenbestaetigung anzeigen/schalten
Satellit: - fritzbox.backup_all: alle Boxen mit hinterlegtem Login nacheinander; nicht erreichbar (Port 49000), Fehler oder Taste noetig → notieren und weiter. Ergebnis: x von y gesichert + Grund je Box. - fritzbox.second_factor: ist die Zusatzbestaetigung an, gibt es sie, und laesst sie sich per TR-064 schalten (nur wenn die Firmware eine Set- Aktion mit Enable-Argument im Auth-Dienst anbietet). - fritzbox.second_factor_set: schaltet sie (confirmed, Taste per button_wait); sonst not_switchable + Weg ueber die Oberflaeche. Diagnostic (💾-Fenster): Zeile "Tastenbestaetigung: an/aus" mit Aus-/ Einschalten (Rueckfrage, Countdown; nicht schaltbar → Hinweisfenster), Knopf "Alle FritzBoxen sichern" mit Ergebnis je Box. Ausschalten/ Deaktivieren gilt jetzt als gefaehrliche Rueckfrage (roter Knopf). Brain: Beschreibung fuer backup_all (je Satellit, weiter bei Fehlern) und second_factor; Timeout 15 min fuer backup_all. Getestet gegen nachgebaute Box (eine Box unerreichbar → uebersprungen, Taste noetig ohne/mit Warten, Status, Schalten mit und ohne Setter) und das Fenster in headless Chromium. Ob echte FRITZ!OS-Versionen das Schalten per TR-064 anbieten, ist ungeprueft. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+87
-1
@@ -875,6 +875,8 @@ async def _control(action: str, params: dict, devices: list[dict]) -> dict:
|
||||
return await loop.run_in_executor(None, _do_snmp_ports, params)
|
||||
if action == "snmp.info":
|
||||
return await loop.run_in_executor(None, _do_snmp_info, params)
|
||||
if action == "fritzbox.backup_all":
|
||||
return await loop.run_in_executor(None, _fb_backup_all, params, devices)
|
||||
if action.startswith("fritzbox."):
|
||||
# Geraetename ("Fritzbox Keller") → IP, wie bei den anderen Aktionen
|
||||
t = _snmp_target(params)
|
||||
@@ -1716,6 +1718,86 @@ def _fb_restore(ip: str, user: str, pw: str, params: dict) -> dict:
|
||||
"den Satelliten? In ein paar Minuten pruefen, ob die Einstellungen uebernommen sind.")}}
|
||||
|
||||
|
||||
def _fb_reachable(ip: str, timeout: float = 3.0) -> bool:
|
||||
try:
|
||||
with socket.create_connection((ip, 49000), timeout=timeout):
|
||||
return True
|
||||
except OSError:
|
||||
return False
|
||||
|
||||
|
||||
def _fb_backup_all(params: dict, devices: list) -> dict:
|
||||
"""Sichert ALLE FritzBoxen mit hinterlegtem Login, eine nach der anderen.
|
||||
Nicht erreichbar / Fehler / Taste noetig → notieren und WEITER, nie abbrechen."""
|
||||
names = {d.get("ip"): d.get("name") for d in devices if d.get("ip")}
|
||||
boxes = [ip for ip, e in sorted(_CREDS.items()) if (e.get("fritzbox") or {}).get("pass")]
|
||||
if not boxes:
|
||||
return {"ok": False, "error": "An diesem Satelliten ist fuer keine FritzBox ein Login hinterlegt."}
|
||||
report = []
|
||||
for ip in boxes:
|
||||
name = names.get(ip) or ip
|
||||
if not _fb_reachable(ip):
|
||||
report.append({"box": name, "ip": ip, "ok": False, "grund": "nicht erreichbar (aus/weg?) — uebersprungen"})
|
||||
continue
|
||||
fb = _creds_for(ip).get("fritzbox", {})
|
||||
try:
|
||||
r = _fb_backup(ip, str(fb.get("user") or ""), str(fb.get("pass") or ""),
|
||||
{**params, "name": name, "no_file": True})
|
||||
except Exception as exc:
|
||||
r = {"ok": False, "error": f"Fehler: {exc}"}
|
||||
if r.get("ok"):
|
||||
report.append({"box": name, "ip": ip, "ok": True, "datei": os.path.basename(r["result"]["datei"])})
|
||||
else:
|
||||
grund = ("braucht eine Taste an der Box — uebersprungen (mit button_wait einzeln nachholen)"
|
||||
if r.get("needs_button") else r.get("error") or "Fehler")
|
||||
report.append({"box": name, "ip": ip, "ok": False, "grund": grund})
|
||||
ok = sum(1 for x in report if x["ok"])
|
||||
return {"ok": True, "result": {"gesichert": ok, "von": len(report), "boxen": report}}
|
||||
|
||||
|
||||
# ─── Zusatzbestaetigung (Taste) der Box: Status + Umschalten ───
|
||||
|
||||
def _fb_second_factor_info(ip: str, user: str, pw: str) -> dict:
|
||||
"""Ist die Tastenbestaetigung an? Und laesst sie sich per TR-064 schalten?
|
||||
Umschalten geht nur, wenn die Firmware dafuer eine Aktion anbietet (eine
|
||||
Set-Aktion des Auth-Dienstes mit einem Enable-Argument) — sonst nur in der
|
||||
FritzBox-Oberflaeche."""
|
||||
auth = _fb_find_service(ip, "X_AVM-DE_Auth")
|
||||
if not auth:
|
||||
return {"ok": True, "result": {"vorhanden": False, "an": False, "schaltbar": False,
|
||||
"hinweis": "Die Box kennt keine Zusatzbestaetigung per Schnittstelle — Aktionen laufen ohne Taste."}}
|
||||
acts = _fb_actions(ip, auth)
|
||||
setter = next((a for a in acts if not _fb_is_read(a["name"]) and a["name"] != "SetConfig"
|
||||
and any("enable" in x.lower() for x in a["in"])), None)
|
||||
info = _tr064(ip, user, pw, auth["type"], auth["control"], "GetInfo") \
|
||||
if any(a["name"] == "GetInfo" for a in acts) else {"ok": False}
|
||||
f = info.get("fields", {}) if info.get("ok") else {}
|
||||
en = f.get("NewEnabled", f.get("NewEnable"))
|
||||
return {"ok": True, "result": {
|
||||
"vorhanden": True, "an": None if en is None else en in ("1", "true"),
|
||||
"schaltbar": bool(setter), "aktion": setter["name"] if setter else "",
|
||||
"hinweis": ("" if setter else
|
||||
"Laesst sich ueber die Schnittstelle nicht umschalten (Firmware bietet es nicht an). "
|
||||
"Nur in der FritzBox-Oberflaeche: System → FRITZ!Box-Benutzer → Anmeldung im Heimnetz → "
|
||||
"\"Ausfuehrung bestimmter Einstellungen und Funktionen zusaetzlich bestaetigen\". "
|
||||
"Manche Firmware erlaubt das Abschalten gar nicht mehr.")}}
|
||||
|
||||
|
||||
def _fb_second_factor_set(ip: str, user: str, pw: str, params: dict) -> dict:
|
||||
inf = _fb_second_factor_info(ip, user, pw)["result"]
|
||||
if not inf["vorhanden"]:
|
||||
return {"ok": False, "error": inf["hinweis"]}
|
||||
if not inf["schaltbar"]:
|
||||
return {"ok": False, "not_switchable": True, "error": inf["hinweis"]}
|
||||
auth = _fb_find_service(ip, "X_AVM-DE_Auth")
|
||||
act = next(a for a in _fb_actions(ip, auth) if a["name"] == inf["aktion"])
|
||||
arg = next(x for x in act["in"] if "enable" in x.lower())
|
||||
on = str(params.get("enable")).lower() in ("1", "true", "an", "on", "ja")
|
||||
return _fb_call(ip, user, pw, {"service": auth["short"], "action": act["name"],
|
||||
"args": {arg: 1 if on else 0}, "confirmed": params.get("confirmed"),
|
||||
"button_wait": params.get("button_wait")})
|
||||
|
||||
|
||||
def _fb_backup_delete(params: dict) -> dict:
|
||||
path = _fb_backup_path(params.get("file") or params.get("datei") or "")
|
||||
if not path:
|
||||
@@ -1763,6 +1845,10 @@ def _do_fritzbox(action: str, params: dict) -> dict:
|
||||
return _fb_backup(ip, user, pw, params)
|
||||
if action == "fritzbox.restore":
|
||||
return _fb_restore(ip, user, pw, params)
|
||||
if action == "fritzbox.second_factor":
|
||||
return _fb_second_factor_info(ip, user, pw)
|
||||
if action == "fritzbox.second_factor_set":
|
||||
return _fb_second_factor_set(ip, user, pw, params)
|
||||
|
||||
if action == "fritzbox.hosts":
|
||||
p = _tr064(ip, user, pw, "urn:dslforum-org:service:Hosts:1",
|
||||
@@ -2679,7 +2765,7 @@ class Satellite:
|
||||
# (dial.launch loest ein Geraet auf, oder es wurde ein device-Ref
|
||||
# mitgegeben). http.get/http.post/wol arbeiten direkt mit url/mac —
|
||||
# ein voller LAN-Scan davor kostete nur unnoetig viele Sekunden.
|
||||
needs_devices = (action == "dial.launch" or bool(params.get("device"))
|
||||
needs_devices = (action in ("dial.launch", "fritzbox.backup_all") or bool(params.get("device"))
|
||||
or (action.startswith("fritzbox.")
|
||||
and not re.match(r"^\d{1,3}(\.\d{1,3}){3}$", str(params.get("ip") or ""))))
|
||||
devices = await self._scan() if needs_devices else self._devices
|
||||
|
||||
Reference in New Issue
Block a user