import argparse import datetime import subprocess import re import os import json import sys # BERSAGLIO (Cloudflare è solitamente il più stabile per i ping) TARGET_HOST = "1.1.1.1" # SOGLIE DI ALLARME LIMIT_LOSS = 5.0 # % di pacchetti persi (sopra il 5% è grave) LIMIT_JITTER = 30.0 # ms di deviazione (sopra 30ms lagga la voce/gioco) # File di stato STATE_FILE = "/home/daniely/docker/telegram-bot/quality_state.json" LOG_FILE = "/home/daniely/docker/telegram-bot/quality_log.txt" def log_line(message: str) -> None: ts = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") try: with open(LOG_FILE, "a", encoding="utf-8") as f: f.write(f"{ts} {message}\n") except Exception: pass def send_alert(title: str, body: str, severity: str = "warning") -> None: """Invia alert alla WebApp Loogle Casa.""" sys.path.insert(0, "/home/daniely/docker/shared") from loogle_core.alert_dispatcher import dispatch_alert dispatch_alert(title, body, category="net_quality", severity=severity) def load_state(): if os.path.exists(STATE_FILE): try: with open(STATE_FILE, "r") as f: return json.load(f) except Exception: pass return {"alert_active": False} def save_state(active): try: with open(STATE_FILE, "w") as f: json.dump({"alert_active": active}, f) except Exception: pass def measure_quality(dry_run: bool = False): print("--- Avvio Test Qualità Linea ---") log_line("INFO Avvio Test Qualità Linea") cmd = f"ping -c 50 -i 0.2 -q -w 15 {TARGET_HOST}" try: output = subprocess.check_output(cmd, shell=True).decode("utf-8") except subprocess.CalledProcessError as e: output = e.output.decode("utf-8") if e.output else "" if not output: print("Errore critico: Nessuna connessione.") return loss_match = re.search(r"([0-9]+(?:[\\.,][0-9]+)?)% packet loss", output) if loss_match: loss_raw = loss_match.group(1).replace(",", ".") try: loss = float(loss_raw) except Exception: loss = 100.0 else: loss = 100.0 if loss < 0: loss = 0.0 if loss > 100: loss = 100.0 jitter = 0.0 rtt_match = re.search( r"rtt min/avg/max/mdev = ([\d\.]+)/([\d\.]+)/([\d\.]+)/([\d\.]+)", output ) if rtt_match: avg_ping = float(rtt_match.group(2)) jitter = float(rtt_match.group(4)) else: avg_ping = 0.0 result_line = f"Risultati: Loss={loss:.2f}% | Jitter={jitter}ms | AvgPing={avg_ping}ms" print(result_line) log_line(f"INFO {result_line}") # Ping senza risposte (es. iptables DROP icmp locale) → misura non affidabile. if loss >= 99.0 and avg_ping == 0.0 and jitter == 0.0: print("Misura non affidabile (ping senza risposte, possibile blocco ICMP locale).") log_line("WARN Misura non affidabile: ping senza risposte") return state = load_state() was_active = state.get("alert_active", False) is_bad = (loss >= LIMIT_LOSS) or (jitter >= LIMIT_JITTER) if is_bad: if not was_active: body_parts = ["Degrado qualità linea rilevato."] if loss >= LIMIT_LOSS: body_parts.append(f"Packet loss: {loss:.2f}% (soglia {LIMIT_LOSS}%)") if jitter >= LIMIT_JITTER: body_parts.append(f"Jitter: {jitter}ms (soglia {LIMIT_JITTER}ms)") body_parts.append(f"Ping medio: {avg_ping}ms") body = "\n".join(body_parts) if not dry_run: send_alert("Degrado qualità linea", body, severity="warning") save_state(True) print("Allarme inviato." if not dry_run else "Allarme (dry-run).") log_line("WARN Allarme inviato") else: print("Qualità ancora scarsa (già notificato).") log_line("WARN Qualità ancora scarsa (già notificato)") elif was_active and not is_bad: body = ( f"Qualità linea ripristinata.\n" f"Ping: {avg_ping}ms | Jitter: {jitter}ms | Loss: {loss:.2f}%" ) if not dry_run: send_alert("Qualità linea ripristinata", body, severity="info") save_state(False) print("Recovery inviata." if not dry_run else "Recovery (dry-run).") log_line("INFO Recovery inviata") else: print("Linea OK.") log_line("INFO Linea OK") if __name__ == "__main__": parser = argparse.ArgumentParser(description="Network quality monitor") parser.add_argument("--dry-run", action="store_true", help="Esegui senza inviare notifiche") args = parser.parse_args() measure_quality(dry_run=args.dry_run)