Real-time alerts for security events and cluster health โ sent directly to the team Telegram group with snapshots, confidence scores, and actionable information.
The EdgeGuard Telegram Bot sends real-time notifications to the team's Telegram group whenever something important happens in the system. There are two categories of alerts: security alerts triggered by AI detection events, and cluster health alerts triggered by infrastructure problems.
The bot is integrated directly into the Flask backend โ no separate process required. A background thread runs continuously inside the backend, monitoring cluster health every 30 seconds and sending Telegram messages when thresholds are crossed or nodes go offline. Security alerts are triggered immediately when the inference service posts a detection event to the backend.
Telegram Bot API is free, requires no account for recipients (just joining a group), works globally without VPN, supports image attachments in the same message as text, and has a simple HTTP API that requires no SDK โ just a POST request. The entire bot integration is under 50 lines of Python.
Telegram provides a simple HTTP API for bots. You create a bot via @BotFather on Telegram โ it gives you a unique token. You then send HTTP POST requests to Telegram's servers with your token, and Telegram delivers the message to any chat or group the bot has been added to.
The bot does not need to run a server or listen for connections โ it only needs outbound internet access from Pi5. The Pi5 connects to its WiFi hotspot which provides internet access, so all Telegram API calls go out through wlan0.
import requests TELEGRAM_TOKEN = "your_bot_token" # from @BotFather CHAT_ID = "your_group_chat_id" # team Telegram group BASE_URL = f"https://api.telegram.org/bot{TELEGRAM_TOKEN}" # Send text message def send_message(text): requests.post( f"{BASE_URL}/sendMessage", json={ "chat_id": CHAT_ID, "text": text, "parse_mode": "HTML" # supports <b>, <i>, <code> }, timeout=5 ) # Send message with image (for security alerts) def send_photo(image_bytes, caption): requests.post( f"{BASE_URL}/sendPhoto", data={ "chat_id": CHAT_ID, "caption": caption, "parse_mode": "HTML" }, files={"photo": ("snapshot.jpg", image_bytes, "image/jpeg")}, timeout=10 )
The bot sends two categories of alerts โ security events from AI detection, and cluster health events from infrastructure monitoring.
Face covered detected by YOLOv8n. Includes snapshot image, confidence score, timestamp, and camera ID.
A Pi3 node stopped responding to Prometheus scrapes โ node_up == 0 for more than 30 seconds.
A k3s pod is in CrashLoopBackOff or Error state โ backend, frontend, inference, or any other service.
Any Pi3 node exceeds 75ยฐC โ risk of CPU throttling which would affect benchmark results.
Cluster average CPU exceeds the auto-scaler threshold โ Pi4 is joining the cluster as emergency node.
A previously offline node has come back online โ sent once when Prometheus detects node_up == 1 again.
A background thread runs inside the Flask backend process, checking cluster health every 30 seconds. It maintains a state dictionary of which nodes and pods were previously healthy โ so it only sends an alert when something changes (goes from healthy to unhealthy, or recovers), not on every polling cycle.
import threading import requests import subprocess import json PROMETHEUS_URL = "http://prometheus-service:9090" NODES = [ {"name": "pi3-01", "ip": "10.10.10.21"}, {"name": "pi3-02", "ip": "10.10.10.22"}, # ... all 8 Pi3 nodes ] node_state = {} # tracks which nodes were last seen online pod_state = {} # tracks which pods were last seen Running def query_prometheus(query): r = requests.get( f"{PROMETHEUS_URL}/api/v1/query", params={"query": query}, timeout=5 ) return r.json()["data"]["result"] def check_nodes(): # Query Prometheus: which nodes are UP? results = query_prometheus("up{job='node-exporter'}") online_nodes = {r["metric"]["instance"]: r["value"][1] == "1" for r in results} for node in NODES: instance = f"{node['ip']}:9100" is_online = online_nodes.get(instance, False) was_online = node_state.get(node["name"], True) if was_online and not is_online: # Node just went offline send_message(f"๐ด <b>NODE OFFLINE</b>\n\n" f"๐ฅ๏ธ <b>Node:</b> {node['name']} ({node['ip']})\n" f"โฑ๏ธ <b>Detected at:</b> {datetime.now()}") elif not was_online and is_online: # Node recovered send_message(f"โ <b>NODE RECOVERED</b>\n\n" f"๐ฅ๏ธ <b>Node:</b> {node['name']} ({node['ip']})") node_state[node["name"]] = is_online def check_pods(): # Get all pods and check for non-Running states result = subprocess.run( ["kubectl", "get", "pods", "-A", "-o", "json"], capture_output=True, text=True ) pods = json.loads(result.stdout)["items"] for pod in pods: name = pod["metadata"]["name"] phase = pod["status"].get("phase", "Unknown") node = pod["spec"].get("nodeName", "unknown") was_ok = pod_state.get(name, True) is_ok = phase in ["Running", "Succeeded"] if was_ok and not is_ok: # Pod just crashed restarts = pod["status"]["containerStatuses"][0]["restartCount"] send_message(f"โ <b>POD CRASHED</b>\n\n" f"๐ฆ <b>Pod:</b> {name}\n" f"๐ฅ๏ธ <b>Node:</b> {node}\n" f"๐ฅ <b>Status:</b> {phase}\n" f"๐ <b>Restarts:</b> {restarts}") pod_state[name] = is_ok def check_temperature(): # Query Prometheus for temperature > 75ยฐC (75000 millidegrees) results = query_prometheus( "node_thermal_zone_temp{job='node-exporter'} > 75000" ) for r in results: node = r["metric"]["instance"] temp = float(r["value"][1]) / 1000 send_message(f"๐ก๏ธ <b>HIGH TEMPERATURE</b>\n\n" f"๐ฅ๏ธ <b>Node:</b> {node}\n" f"๐ก๏ธ <b>Temperature:</b> {temp:.1f}ยฐC\n" f"โ ๏ธ Risk of CPU throttling above 80ยฐC") def health_monitor_loop(): while True: try: check_nodes() check_pods() check_temperature() except Exception as e: print(f"Health monitor error: {e}") threading.Event().wait(30) # check every 30 seconds # Start background thread when Flask app starts monitor_thread = threading.Thread(target=health_monitor_loop, daemon=True) monitor_thread.start()
# Called when inference service POSTs to /api/alerts def send_security_alert(alert, image_bytes): caption = ( f"๐จ <b>SUSPICIOUS EVENT DETECTED</b>\n\n" f"๐ท <b>Camera:</b> {alert['camera_id']}\n" f"๐ฏ <b>Confidence:</b> {alert['confidence']*100:.1f}%\n" f"๐ท๏ธ <b>Event:</b> {alert['event_type']}\n" f"๐ <b>Time:</b> {alert['timestamp']}\n" f"๐ <b>Alert ID:</b> #{alert['id']}" ) if image_bytes: send_photo(image_bytes, caption) # image + text together else: send_message(caption) # text only if no image
/newbot and follow the prompts. Choose a name and username for the bot. BotFather gives you a token like 1234567890:ABCdefGHI..."chat":{"id": -1234567890} โ the negative number is your Chat ID.| Alert Type | Trigger Condition | Data Source | Includes Image? | Recovery Alert? |
|---|---|---|---|---|
| ๐จ Suspicious Detection | face_covered with confidence > 50% | Inference service โ /api/alerts | Yes โ snapshot | No |
| ๐ด Node Offline | node_up == 0 for > 30s | Prometheus | No | Yes |
| โ Pod Crashed | Pod phase != Running/Succeeded | kubectl get pods | No | Yes |
| ๐ก๏ธ High Temperature | node_thermal_zone_temp > 75ยฐC | Prometheus | No | No (alert per cycle) |
| ๐ฅ High CPU | Cluster avg CPU > threshold | Prometheus | No | Yes (Pi4 left) |
The same Telegram bot handles both AI detection alerts (with snapshots) and infrastructure alerts (node down, pod crashed, high temperature). The team gets one notification channel for everything happening in EdgeGuard.
The health monitor tracks previous state for every node and pod. It only sends a Telegram message when something changes โ going offline or recovering. Without this, a node that stays offline would send an alert every 30 seconds flooding the group chat.
All Telegram API calls go out through Pi5's wlan0 (WiFi hotspot connection). If Pi5 loses internet access, alerts cannot be delivered even if the cluster itself is healthy on the LAN. The health monitor continues running and will resume sending alerts when internet is restored.
Running the health monitor as a background thread inside Flask means no extra pod, no extra Dockerfile, no extra k3s Deployment to manage. The daemon=True flag ensures the thread stops automatically when Flask stops โ no zombie processes. The tradeoff is that if both Flask replicas crash simultaneously, monitoring stops until a replica recovers.