Später · 21 Neue Geräte im Heimnetz (Nachbartabelle des Proxmox-Hosts statt AdGuard)
AdGuard sieht in diesem Netz fast nur die Fritzbox, die die Anfragen der meisten Geräte weiterreicht (nachgesehen 26.09.). Deshalb: - Ausführer: alle 30 min Ping an jede Adresse des /24 der Brücke vmbr0, dann ip neigh (MAC, IP, Rückwärtsname); nur lesend, im Bericht als host.netz (größer als /24 wird nicht abgesucht) - services/homelab/netzgeraete.py: Register netz-bekannt.json; erster Abgleich = alles bekannt, Gäste des Hosts und neue MACs mit bekanntem Namen (Handys mit zufälliger MAC) gelten als bekannt; Neues einmal gemeldet - Wächter: gelber Hinweis „Neues Gerät im Netz“ mit „Ist bekannt“; GET /api/homelab/netz Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
241e91c03f
commit
5325544d89
@@ -0,0 +1,166 @@
|
||||
"""Neue Geräte im Heimnetz (Ausbauplan „Später“, Punkt 21, seit 26.09.2026).
|
||||
|
||||
Geplant war AdGuard als Quelle. Nachgesehen am 26.09.: AdGuard sieht in diesem Netz fast nur die Fritzbox, die die
|
||||
Anfragen der meisten Geräte weiterreicht. Der Ausführer sucht deshalb alle 30 Minuten das Netz der Proxmox-Brücke ab
|
||||
(Ping an jede Adresse, dann die Nachbartabelle; nur lesend) und schickt die Geräte mit MAC, IP und Namen im Bericht mit
|
||||
(host.netz). Hier wird jede MAC mit dem Register <Datenordner>/netz-bekannt.json verglichen:
|
||||
• Beim allerersten Abgleich gilt alles als bekannt — sonst stünden alle Geräte auf einmal da.
|
||||
• Eine neue MAC wird „neu“: Lucy meldet sie einmal (nicht dringend, nachts mit der Morgenmeldung), und der Wächter
|
||||
zeigt einen gelben Hinweis „Neues Gerät im Netz“ mit dem Knopf „Ist bekannt“, bis jemand ihn drückt.
|
||||
• Handys wechseln ihre MAC (zufällige, „lokal verwaltete“ Adressen). Trägt eine neue MAC denselben Namen wie ein
|
||||
bekanntes Gerät, gilt sie als dasselbe Gerät.
|
||||
• Die Gäste des Proxmox-Hosts kennt der Orchestrator ohnehin; ihre IPs gelten als bekannt.
|
||||
Verschwundene Geräte bleiben im Register (mit „zuletzt gesehen“): Ein Gerät, das nur ab und zu da ist, soll nicht jedes
|
||||
Mal neu sein. Geschrieben wird nur im Prozess der Oberfläche (Bericht, Knopf); der Wächter im Steward liest.
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
import threading
|
||||
import time
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
from kern.einstellungen import einstellungen
|
||||
from kern.zeit import LOCAL_TZ
|
||||
|
||||
from services import announce
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
BETREFF = "[Homelab]"
|
||||
_lock = threading.Lock()
|
||||
|
||||
|
||||
def _pfad() -> Path:
|
||||
return einstellungen().daten_dir / "netz-bekannt.json"
|
||||
|
||||
|
||||
def _lesen() -> dict | None:
|
||||
try:
|
||||
daten = json.loads(_pfad().read_text(encoding="utf-8"))
|
||||
except (OSError, ValueError):
|
||||
return None
|
||||
return daten if isinstance(daten, dict) and isinstance(daten.get("geraete"), dict) else None
|
||||
|
||||
|
||||
def _schreiben(daten: dict) -> None:
|
||||
pfad = _pfad()
|
||||
pfad.parent.mkdir(parents=True, exist_ok=True)
|
||||
tmp = pfad.with_suffix(".tmp")
|
||||
tmp.write_text(json.dumps(daten, ensure_ascii=False, indent=1), encoding="utf-8")
|
||||
tmp.replace(pfad)
|
||||
|
||||
|
||||
def lokal_verwaltet(mac: str) -> bool:
|
||||
"""Zufällige MAC (zweites Bit des ersten Bytes gesetzt) — so schützen Handys ihre Privatsphäre."""
|
||||
try:
|
||||
return bool(int(mac.split(":")[0], 16) & 0x02)
|
||||
except (ValueError, IndexError):
|
||||
return False
|
||||
|
||||
|
||||
def _kurzname(name: str | None) -> str | None:
|
||||
return name.removesuffix(".fritz.box") if name else None
|
||||
|
||||
|
||||
def abgleichen(bericht: dict, jetzt: float | None = None) -> list[dict]:
|
||||
"""Nach jedem Bericht: Register nachführen. Rückgabe: die Geräte, die eben neu dazukamen (für die Meldung)."""
|
||||
jetzt = time.time() if jetzt is None else jetzt
|
||||
b = bericht.get("bericht") if isinstance(bericht.get("bericht"), dict) else bericht
|
||||
netz = ((b or {}).get("host") or {}).get("netz")
|
||||
if not isinstance(netz, dict) or not isinstance(netz.get("geraete"), list) or not netz.get("netz"):
|
||||
return []
|
||||
gast_ips = {str(g.get("ip")) for g in (b or {}).get("gaeste") or [] if isinstance(g, dict) and g.get("ip")}
|
||||
neu: list[dict] = []
|
||||
with _lock:
|
||||
daten = _lesen()
|
||||
erstes_mal = daten is None
|
||||
daten = daten or {"geraete": {}}
|
||||
register: dict = daten["geraete"]
|
||||
namen = {e.get("name"): mac for mac, e in register.items() if e.get("name")}
|
||||
for g in netz["geraete"]:
|
||||
mac, ip, name = str(g.get("mac") or "").lower(), str(g.get("ip") or ""), g.get("name")
|
||||
if not mac or not ip:
|
||||
continue
|
||||
if mac in register:
|
||||
register[mac].update(ip=ip, zuletzt=jetzt, **({"name": name} if name else {}))
|
||||
continue
|
||||
gleicher_name = name and namen.get(name)
|
||||
bekannt = erstes_mal or ip in gast_ips or bool(gleicher_name)
|
||||
register[mac] = {"ip": ip, "name": name, "erstmals": jetzt, "zuletzt": jetzt, "bestaetigt": bekannt}
|
||||
if gleicher_name:
|
||||
register[mac]["wie"] = gleicher_name
|
||||
if not bekannt:
|
||||
neu.append({"mac": mac, **register[mac]})
|
||||
daten["netz"], daten["stand"] = netz["netz"], float(netz.get("zeit") or jetzt)
|
||||
_schreiben(daten)
|
||||
for g in neu:
|
||||
log.info("netz: neues Gerät %s (%s, %s)", g["ip"], g["mac"], g.get("name") or "ohne Namen")
|
||||
if neu:
|
||||
_melden(neu)
|
||||
return neu
|
||||
|
||||
|
||||
def _beschreibung(g: dict) -> str:
|
||||
teile = [g["ip"], f"MAC {g['mac']}"]
|
||||
if lokal_verwaltet(g["mac"]):
|
||||
teile.append("zufällige MAC, oft ein Handy oder Tablet")
|
||||
return " · ".join(teile)
|
||||
|
||||
|
||||
def _melden(neu: list[dict]) -> None:
|
||||
zeilen = [f"• {_kurzname(g.get('name')) or 'ohne Namen'} ({_beschreibung(g)})" for g in neu[:8]]
|
||||
text = (("Ein neues Gerät ist" if len(neu) == 1 else f"{len(neu)} neue Geräte sind") + " im Heimnetz aufgetaucht:\n"
|
||||
+ "\n".join(zeilen) + "\nKennst du es, drück im Homelab-Cockpit „Ist bekannt“.")
|
||||
try:
|
||||
announce.notify_telegram(BETREFF, text)
|
||||
except Exception:
|
||||
log.warning("netz: Meldung ging nicht raus", exc_info=True)
|
||||
|
||||
|
||||
def bestaetigen(mac: str) -> dict:
|
||||
"""Knopf „Ist bekannt“ (Aktion des Hinweises)."""
|
||||
mac = str(mac or "").lower()
|
||||
with _lock:
|
||||
daten = _lesen()
|
||||
eintrag = (daten or {}).get("geraete", {}).get(mac)
|
||||
if not daten or not eintrag:
|
||||
return {"ok": False, "detail": "Dieses Gerät kennt das Register nicht."}
|
||||
eintrag["bestaetigt"] = True
|
||||
_schreiben(daten)
|
||||
return {"ok": True, "text": f"Gemerkt: {_kurzname(eintrag.get('name')) or eintrag['ip']} ist bekannt. Der Hinweis "
|
||||
"verschwindet in wenigen Minuten."}
|
||||
|
||||
|
||||
def lage() -> dict:
|
||||
"""Für GET /api/homelab/netz: alle Geräte, neue zuerst, dann nach IP."""
|
||||
daten = _lesen() or {"geraete": {}}
|
||||
geraete = [{"mac": mac, **e, "name": _kurzname(e.get("name")), "zufaellig": lokal_verwaltet(mac)}
|
||||
for mac, e in daten["geraete"].items()]
|
||||
|
||||
def ip_schluessel(g: dict) -> tuple:
|
||||
try:
|
||||
return tuple(int(t) for t in g["ip"].split("."))
|
||||
except (ValueError, AttributeError):
|
||||
return (999,)
|
||||
geraete.sort(key=lambda g: (g.get("bestaetigt", True), ip_schluessel(g)))
|
||||
return {"netz": daten.get("netz"), "stand": daten.get("stand"), "geraete": geraete}
|
||||
|
||||
|
||||
def pruefe_netzgeraete() -> list:
|
||||
"""Für den Wächter (Rolle homelab): jedes unbestätigte Gerät als gelber Hinweis mit dem Knopf „Ist bekannt“."""
|
||||
from services.waechter import Befund, _aktion
|
||||
|
||||
befunde = []
|
||||
for mac, e in ((_lesen() or {}).get("geraete") or {}).items():
|
||||
if e.get("bestaetigt", True):
|
||||
continue
|
||||
g = {"mac": mac, **e}
|
||||
seit = datetime.fromtimestamp(float(e.get("erstmals") or 0), LOCAL_TZ).strftime("%d.%m. %H:%M")
|
||||
befunde.append(Befund(
|
||||
id=f"netz:{mac}", stufe="gelb", titel=f"Neues Gerät im Netz: {_kurzname(e.get('name')) or e.get('ip')}",
|
||||
text=f"{_beschreibung(g)}, zuerst gesehen {seit}. Kennst du es, drück „Ist bekannt“; sonst in der Fritzbox "
|
||||
"nachsehen, wem es gehört.",
|
||||
quelle="netz", aktionen=[_aktion("bekannt", "Ist bekannt", mac=mac)], sofort=True))
|
||||
return befunde
|
||||
Reference in New Issue
Block a user