-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathpresence.py
More file actions
102 lines (86 loc) · 3.92 KB
/
Copy pathpresence.py
File metadata and controls
102 lines (86 loc) · 3.92 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
"""
presence.py – "wieder online"-Meldung fuer getaggte known/unknown-Geraete.
arpwatch meldet ein Geraet nur EINMAL (new station) und schweigt danach. Hier
machen wir es session-basiert ueber `hosts.last_seen` (das beide Ingester bei
jeder Aktivitaet aktualisieren): war ein Geraet idle und wird wieder aktiv ->
EINE Meldung an HA. Gemeldet werden NUR Geraete mit gesetztem "Tracked"-Haken
(opt-in pro Geraet) — voellig unabhaengig vom rein kosmetischen Tag. Alles andere
(Default) bleibt still.
Hysterese gegen Flattern: aktiv = last_seen < ACTIVE_WINDOW, abwesend =
last_seen > ABSENT_GAP, dazwischen Zustand halten. Erstes Sehen wird still
geseedet (kein Alarm-Burst auf bereits anwesende Geraete). systemd-Timer alle 5 min.
"""
import json
import os
import time
import urllib.request
import aliases
import config
import db
import vlans
ACTIVE_WINDOW = 600 # s: last_seen juenger -> sicher aktiv
ABSENT_GAP = 1800 # s: last_seen aelter -> sicher abwesend (=Session zu Ende)
def _env_webhook():
try:
for line in open("/opt/netmon/.env", encoding="utf-8"):
line = line.strip()
if line.startswith("HA_WEBHOOK_URL="):
return line.split("=", 1)[1].strip().strip('"').strip("'")
except FileNotFoundError:
pass
return None
def _ha_post(title, msg):
url = os.environ.get("HA_WEBHOOK_URL") or _env_webhook()
if not url or "PLACEHOLDER" in url:
print(f"[presence] (kein Webhook) {title}: {msg}", flush=True)
return
data = json.dumps({"title": title, "message": msg, "source": "presence"}).encode()
try:
urllib.request.urlopen(
urllib.request.Request(url, data=data,
headers={"Content-Type": "application/json"}),
timeout=5)
except Exception as e:
print(f"[presence] HA-POST fehlgeschlagen: {e}", flush=True)
def main():
conn = db.connect(config.DB_PATH)
now = time.time()
tracked = aliases.load_tracked(conn)
tags = aliases.load_tags(conn)
names = aliases.load_names(conn, config.ALIASES_TSV, config.DHCP_NAMES)
state = {r["ip"]: r["present"] for r in conn.execute("SELECT ip,present FROM presence")}
for h in conn.execute("SELECT ip,last_seen FROM hosts WHERE last_seen IS NOT NULL").fetchall():
ip = h["ip"]
# Verhalten haengt NUR am Tracked-Haken, NICHT am kosmetischen Tag.
# Nicht-getrackte Geraete (Default) bleiben komplett still.
if ip not in tracked:
continue
age = now - h["last_seen"]
prev = state.get(ip) # None = noch nie geseedet
if age < ACTIVE_WINDOW:
active = True
elif age > ABSENT_GAP:
active = False
else:
active = bool(prev) if prev is not None else False
if prev is None:
conn.execute("INSERT OR IGNORE INTO presence(ip,present,since,last_notify) "
"VALUES(?,?,?,NULL)", (ip, 1 if active else 0, now))
continue
if active and not prev: # abwesend -> aktiv = wieder online
v = vlans.vlan_of(ip)
vid = v["id"] if v else None
nm = names.get(ip, ip)
tag = tags.get(ip) # rein kosmetisch, nur Kontext in der Meldung
_ha_post("netmon: getracktes Gerät wieder online",
f"{nm} ({ip}{', VLAN ' + str(vid) if vid else ''}) ist wieder aktiv.")
conn.execute("INSERT INTO presence_events(ts,ip,trust,vlan,kind) VALUES(?,?,?,?,?)",
(now, ip, tag, vid, "reappear"))
conn.execute("UPDATE presence SET present=1, since=?, last_notify=? WHERE ip=?",
(now, now, ip))
elif not active and prev: # aktiv -> abwesend (still, nur Zustand)
conn.execute("UPDATE presence SET present=0, since=? WHERE ip=?", (now, ip))
conn.commit()
conn.close()
if __name__ == "__main__":
main()