Task 9 ยท Telegram Bot

Telegram Notification Bot

Real-time alerts for security events and cluster health โ€” sent directly to the team Telegram group with snapshots, confidence scores, and actionable information.

๐Ÿ‘ค Lead: Negar Mohammadi, Ikbela Halili, Krish โœ“ Complete Telegram Bot API ยท Python ยท Background Thread

Overview

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.

๐Ÿ“ฑ Why Telegram and not email or Slack?

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.

How the Telegram Bot API Works

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.

pythonCore Telegram send functions
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
    )

Alert Types

The bot sends two categories of alerts โ€” security events from AI detection, and cluster health events from infrastructure monitoring.

๐Ÿšจ

Suspicious Detection

Face covered detected by YOLOv8n. Includes snapshot image, confidence score, timestamp, and camera ID.

Trigger: inference โ†’ POST /api/alerts
๐Ÿ“ด

Node Offline

A Pi3 node stopped responding to Prometheus scrapes โ€” node_up == 0 for more than 30 seconds.

Trigger: Prometheus node_up == 0
โŒ

Pod Crashed

A k3s pod is in CrashLoopBackOff or Error state โ€” backend, frontend, inference, or any other service.

Trigger: kubectl pod status != Running
๐ŸŒก๏ธ

High Temperature

Any Pi3 node exceeds 75ยฐC โ€” risk of CPU throttling which would affect benchmark results.

Trigger: node_thermal_zone_temp > 75000
๐Ÿ”ฅ

High CPU

Cluster average CPU exceeds the auto-scaler threshold โ€” Pi4 is joining the cluster as emergency node.

Trigger: avg CPU > threshold %
โœ…

Node Recovered

A previously offline node has come back online โ€” sent once when Prometheus detects node_up == 1 again.

Trigger: node_up returns to 1

Message Format โ€” Examples

Security Alert โ€” Face Covered Detected

๐Ÿ“ฑ Telegram Message (with snapshot image attached)
๐Ÿšจ SUSPICIOUS EVENT DETECTED

๐Ÿ“ท Camera: sensor-node (Pi4)
๐ŸŽฏ Confidence: 94.3%
๐Ÿท๏ธ Event: face_covered
๐Ÿ• Time: 2026-07-11 14:23:07
๐Ÿ”— Alert ID: #247

View in EdgeGuard dashboard for full details.

Node Offline Alert

๐Ÿ“ฑ Telegram Message (text only)
๐Ÿ“ด NODE OFFLINE

๐Ÿ–ฅ๏ธ Node: pi3-04 (10.10.10.24)
โฑ๏ธ Detected at: 2026-07-11 14:25:12
๐Ÿ“Š Last seen: 2026-07-11 14:24:41

โš ๏ธ This node is no longer responding to health checks. k3s will attempt to reschedule any pods that were running on this node.

Pod Crashed Alert

๐Ÿ“ฑ Telegram Message (text only)
โŒ POD CRASHED

๐Ÿ“ฆ Pod: edgeguard-backend-7d4f9b-xk2p1
๐Ÿ–ฅ๏ธ Node: pi3-03 (10.10.10.23)
๐Ÿ’ฅ Status: CrashLoopBackOff
๐Ÿ”„ Restarts: 3
๐Ÿ• Time: 2026-07-11 14:30:45

k3s is attempting to restart the pod automatically.

Node Recovered

๐Ÿ“ฑ Telegram Message (text only)
โœ… NODE RECOVERED

๐Ÿ–ฅ๏ธ Node: pi3-04 (10.10.10.24)
โฑ๏ธ Recovered at: 2026-07-11 14:28:33
โฌ‡๏ธ Downtime: ~3 minutes

Node is back online and accepting workloads.

Implementation โ€” Cluster Health Monitor

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.

pythonCluster health monitor โ€” background thread
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()

Security Alert โ€” Triggered by Inference Service

pythonSecurity alert with snapshot โ€” triggered on detection
# 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

Setup โ€” How to Create a Telegram Bot

1
Open Telegram and message @BotFatherType /newbot and follow the prompts. Choose a name and username for the bot. BotFather gives you a token like 1234567890:ABCdefGHI...
2
Add the bot to your team Telegram groupSearch for the bot by username, add it to the group, and make it an admin so it can send messages.
3
Get the group Chat ID Send a message in the group, then visit:
https://api.telegram.org/bot<YOUR_TOKEN>/getUpdates
Find "chat":{"id": -1234567890} โ€” the negative number is your Chat ID.
4
Add token and chat ID to backend configSet as environment variables in the k3s Deployment or in a Kubernetes Secret.

Complete Monitoring Summary

Alert TypeTrigger ConditionData SourceIncludes Image?Recovery Alert?
๐Ÿšจ Suspicious Detectionface_covered with confidence > 50%Inference service โ†’ /api/alertsYes โ€” snapshotNo
๐Ÿ“ด Node Offlinenode_up == 0 for > 30sPrometheusNoYes
โŒ Pod CrashedPod phase != Running/Succeededkubectl get podsNoYes
๐ŸŒก๏ธ High Temperaturenode_thermal_zone_temp > 75ยฐCPrometheusNoNo (alert per cycle)
๐Ÿ”ฅ High CPUCluster avg CPU > thresholdPrometheusNoYes (Pi4 left)

Key Findings

โœ… Comprehensive coverage โ€” security and infrastructure in one bot

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.

โœ… State-aware โ€” only alerts on changes not every cycle

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.

โš ๏ธ Requires internet access on Pi5

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.

๐Ÿ” Why a daemon thread and not a separate service?

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.