#!/usr/bin/env python3
"""
mr600.py – LTE-Signalwerte des TP-Link Archer MR600 (und verwandter MR-Modelle) auslesen,
ohne App, ohne Browser. Verifiziert am Archer MR600 v3.0, Firmware 1.3.0 Build 250915.

  pip3 install tplinkrouterc6u          (Bibliothek, macht Login + Verschlüsselung)

  export MR600_PASSWORD='…'             oder --password-file /pfad/zur/datei
  mr600.py status                       Werte lesbar
  mr600.py json                         Werte als JSON (für Skripte)
  mr600.py prometheus                   Metriken im Prometheus-Textformat
  mr600.py prometheus --push http://host:9091/metrics/job/lte_signal
  mr600.py watch --interval 60          Dauerbetrieb (status/json/prometheus per --format)

Optionen: --host http://192.168.1.1  --username admin  --client MR6400v7|MR600|MR200|auto

Fallen (siehe https://bergclimber.de/mr600/):
  * Der Router erlaubt EINE Admin-Session. Dieses Skript loggt sich pro Abfrage ein UND aus.
    Solange jemand in der Web-UI angemeldet ist, scheitert der Login.
  * Nach 5 Fehlversuchen sperrt der Router den Login für längere Zeit. Bei „Passwort falsch"
    (Code 71233) legt watch deshalb 30 Minuten Pause ein, statt es jede Minute zu versuchen.
  * MR600 v3 mit aktueller Firmware braucht den Client MR6400v7 UND username="admin",
    sonst 71233, obwohl das Passwort stimmt.
  * rfInfoSnr kommt in Zehntel-dB (152 = 15,2 dB). SIM-Status 3 „SIM card prepared" ist normal.
"""
import argparse
import json
import os
import sys
import time
import urllib.request

try:
    import tplinkrouterc6u as tp
except ImportError:
    sys.exit("Bibliothek fehlt: pip3 install tplinkrouterc6u")

CLIENTS = {"MR6400v7": "TPLinkMR6400v7Client", "MR600": "TPLinkMR600Client", "MR200": "TPLinkMR200Client"}
AUTH_FAIL_BACKOFF = 1800


def is_auth_reject(e):
    return isinstance(e, AssertionError) or "wrong password" in str(e).lower() or "71233" in str(e)


def make_client(a, password):
    if a.client == "auto":
        return tp.TplinkRouterProvider.get_client(a.host, password, username=a.username, timeout=a.timeout)
    cls = getattr(tp, CLIENTS[a.client])
    return cls(a.host, password, username=a.username, timeout=a.timeout)


def read_status(a, password):
    """Login → LTE-Status → Logout. Logout IMMER, sonst blockiert die Session die Web-UI."""
    c = make_client(a, password)
    c.authorize()
    try:
        st = c.get_lte_status()
        fw = c.get_firmware() if a.firmware else None
    finally:
        try:
            c.logout()
        except Exception:
            pass
    d = {k: v for k, v in vars(st).items() if not k.startswith("_")}
    d["snr_db"] = round(st.snr / 10.0, 1)  # Rohwert in Zehntel-dB
    d["network_type_info"] = st.network_type_info
    d["sim_status_info"] = st.sim_status_info
    if fw:
        d["firmware"] = {"model": fw.model, "hardware": fw.hardware_version, "firmware": fw.firmware_version}
    return d


def rate_rsrp(v):
    return "sehr gut" if v >= -80 else "gut" if v >= -90 else "brauchbar" if v >= -100 else "schwach" if v >= -110 else "Kante"


def rate_snr(v):
    return "sehr gut" if v >= 20 else "gut" if v >= 13 else "brauchbar" if v >= 0 else "schlecht"


def fmt_status(d):
    lines = [
        f"Netz        {d['network_type_info']}  ({d.get('isp_name') or '–'})   Balken {d['sig_level']}/5",
        f"RSRP        {d['rsrp']} dBm   ({rate_rsrp(d['rsrp'])})",
        f"RSRQ        {d['rsrq']} dB",
        f"SNR         {d['snr_db']} dB   ({rate_snr(d['snr_db'])})",
        f"SIM         {d['sim_status']} = {d['sim_status_info']}   Verbindung {d['connect_status']}",
        f"Rate        ↓ {d['cur_rx_speed']} B/s  ↑ {d['cur_tx_speed']} B/s   gesamt {d['total_statistics'] / 1e6:.1f} MB",
        f"SMS ungelesen {d['sms_unread_count']}",
    ]
    if "firmware" in d:
        lines.append(f"Gerät       {d['firmware']['hardware']} · {d['firmware']['firmware']}")
    return "\n".join(lines)


def fmt_prometheus(d, up=1):
    out = []

    def m(name, help_, typ, val, labels=""):
        out.append(f"# HELP {name} {help_}\n# TYPE {name} {typ}\n{name}{{{labels}}} {val}" if labels else f"# HELP {name} {help_}\n# TYPE {name} {typ}\n{name} {val}")

    m("lte_signal_up", "1 wenn die letzte Abfrage erfolgreich war", "gauge", up)
    if d is None:
        return "\n".join(out) + "\n"
    m("lte_rsrp_dbm", "Reference Signal Received Power (dBm)", "gauge", d["rsrp"])
    m("lte_rsrq_db", "Reference Signal Received Quality (dB)", "gauge", d["rsrq"])
    m("lte_snr_db", "Signal-to-Noise Ratio (dB, Router liefert Zehntel)", "gauge", d["snr_db"])
    m("lte_signal_level", "Signalbalken laut Router (0-5)", "gauge", d["sig_level"])
    m("lte_network_type", "Netztyp-Code (0 kein Netz, 3 LTE, 7 LTE-A, 8 5G NR)", "gauge", d["network_type"], f'info="{d["network_type_info"]}"')
    m("lte_connect_status", "Verbindungsstatus-Code (4 = verbunden beobachtet)", "gauge", d["connect_status"])
    m("lte_sim_status", "SIM-Status-Code (3 = SIM card prepared = normal)", "gauge", d["sim_status"])
    m("lte_rx_speed_bytes_per_second", "Empfangsrate laut Router", "gauge", d["cur_rx_speed"])
    m("lte_tx_speed_bytes_per_second", "Senderate laut Router", "gauge", d["cur_tx_speed"])
    m("lte_total_statistics_bytes", "Gesamtvolumen laut Router", "gauge", d["total_statistics"])
    m("lte_sms_unread", "Ungelesene SMS auf dem Router", "gauge", d["sms_unread_count"])
    return "\n".join(out) + "\n"


def emit(a, d, up=1):
    fmt = a.format if a.action == "watch" else a.action
    if fmt == "json":
        text = json.dumps(d, ensure_ascii=False, indent=2) + "\n"
    elif fmt == "prometheus":
        text = fmt_prometheus(d, up)
    else:
        text = (fmt_status(d) if d else "Abfrage fehlgeschlagen") + "\n"
    if a.push and fmt == "prometheus":
        req = urllib.request.Request(a.push, data=text.encode(), method="PUT", headers={"Content-Type": "text/plain"})
        urllib.request.urlopen(req, timeout=10)
    else:
        sys.stdout.write(text)
        sys.stdout.flush()


def main():
    ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
    ap.add_argument("action", choices=["status", "json", "prometheus", "watch"])
    ap.add_argument("--host", default=os.environ.get("MR600_HOST", "http://192.168.1.1"))
    ap.add_argument("--username", default=os.environ.get("MR600_USERNAME", "admin"))
    ap.add_argument("--password-file", help="Datei mit dem Admin-Passwort (sonst Umgebungsvariable MR600_PASSWORD)")
    ap.add_argument("--client", choices=["MR6400v7", "MR600", "MR200", "auto"], default="MR6400v7")
    ap.add_argument("--timeout", type=int, default=15)
    ap.add_argument("--firmware", action="store_true", help="zusätzlich Modell/Firmware abfragen")
    ap.add_argument("--format", choices=["status", "json", "prometheus"], default="status", help="Ausgabeformat für watch")
    ap.add_argument("--interval", type=int, default=60, help="Sekunden zwischen Abfragen bei watch")
    ap.add_argument("--push", help="Pushgateway-URL (nur prometheus), z. B. http://localhost:9091/metrics/job/lte_signal")
    a = ap.parse_args()

    password = open(a.password_file).read().strip() if a.password_file else os.environ.get("MR600_PASSWORD")
    if not password:
        sys.exit("Kein Passwort: MR600_PASSWORD setzen oder --password-file angeben")

    if a.action != "watch":
        try:
            emit(a, read_status(a, password))
        except Exception as e:
            hint = "  (Passwort/Benutzername? Jemand in der Web-UI angemeldet? Nicht sofort wiederholen: nach 5 Fehlversuchen sperrt der Router.)" if is_auth_reject(e) else ""
            sys.exit(f"Fehler: {type(e).__name__}: {e}{hint}")
        return

    blocked_until = 0.0
    while True:
        t0 = time.time()
        d = None
        try:
            if time.time() < blocked_until:
                raise RuntimeError(f"Login-Pause bis {time.strftime('%H:%M:%S', time.localtime(blocked_until))}")
            d = read_status(a, password)
        except Exception as e:
            if is_auth_reject(e):
                blocked_until = time.time() + AUTH_FAIL_BACKOFF
            print(f"{time.strftime('%H:%M:%S')} Fehler: {e}", file=sys.stderr)
        try:
            emit(a, d, up=1 if d else 0)
        except Exception as e:
            print(f"{time.strftime('%H:%M:%S')} Ausgabe/Push fehlgeschlagen: {e}", file=sys.stderr)
        time.sleep(max(5, a.interval - (time.time() - t0)))


if __name__ == "__main__":
    try:
        main()
    except KeyboardInterrupt:
        pass
