Posted on
Artificial Intelligence

Artificial Intelligence Raspberry Pi Monitoring

Author
  • User
    linuxbash
    Posts by this author
    Posts by this author

AI-Powered Raspberry Pi Monitoring with Bash: Catch Problems Before They Break Things

Your Raspberry Pi is the unsung hero of your homelab—running Home Assistant, Pi-hole, a camera, or a custom project 24/7. But tiny boards can silently overheat, throttle, or run out of disk space. By the time you notice, logs are gone, services crashed, or the SD card is corrupted.

This post shows how to add a lightweight “AI guardrail” to your Pi using Bash for telemetry and a tiny anomaly detector that learns what “normal” looks like—so it can alert you when something’s off.

What you’ll build:

  • A Bash telemetry collector that logs temperature, load, memory, disk, and throttling flags to CSV

  • A small scikit-learn Isolation Forest that flags anomalies relative to your Pi’s baseline behavior

  • A cron job that runs every minute and alerts you via a webhook (Slack/Discord/Mattermost) or syslog

Why this matters:

  • Baselines beat thresholds: “75°C” might be fine under a compile, but not at night when idle. AI can learn your Pi’s normal patterns.

  • On-device, no cloud: Everything runs locally on a Pi 3/4/5 without heavyweight stacks.

  • Bash-first: Glue everything with shell scripts you can audit and extend.

1) Install prerequisites

We’ll use standard Linux tools plus Python’s scikit-learn. Pick your package manager.

A) apt (Debian, Ubuntu, Raspberry Pi OS):

sudo apt update
sudo apt install -y \
  lm-sensors sysstat jq curl \
  python3 python3-pip python3-sklearn
# Optional (vcgencmd; Raspberry Pi OS usually has this already):
sudo apt install -y libraspberrypi-bin
# Optional load generator for testing:
sudo apt install -y stress-ng

B) dnf (Fedora):

sudo dnf install -y \
  lm_sensors sysstat jq curl \
  python3 python3-pip python3-scikit-learn
# Optional (vcgencmd, package availability varies by spin/repo):
# sudo dnf install -y raspberrypi-userland
# Optional load generator for testing:
sudo dnf install -y stress-ng

C) zypper (openSUSE):

sudo zypper refresh
sudo zypper install -y \
  lm_sensors sysstat jq curl \
  python3 python3-pip python3-scikit-learn
# Optional (vcgencmd, package name may be raspberrypi-userland or similar):
# sudo zypper install -y raspberrypi-userland
# Optional load generator for testing:
sudo zypper install -y stress-ng

Notes:

  • If your repo lacks scikit-learn, you can install it via pip:

    python3 -m pip install --user --upgrade pip
    python3 -m pip install --user scikit-learn
    
  • Initialize lm-sensors once:

    sudo sensors-detect --auto || true
    

We’ll read temperature from /sys (works across distros on Raspberry Pi), so vcgencmd is optional; when available, we’ll also read throttling flags.

2) Collect telemetry with a simple Bash script

Create /usr/local/bin/pi_telemetry.sh:

#!/usr/bin/env bash
set -euo pipefail

LOG_FILE="/var/log/pi_metrics.csv"
TMPFILE="$(mktemp)"
TIMESTAMP="$(date -Is)"

# CPU temp (millidegrees C -> C). Works on Raspberry Pi.
if [[ -r /sys/class/thermal/thermal_zone0/temp ]]; then
  CPU_TEMP_C="$(awk '{printf "%.2f", $1/1000}' /sys/class/thermal/thermal_zone0/temp)"
else
  # Fallback: try lm-sensors (value may differ by board)
  CPU_TEMP_C="$(sensors 2>/dev/null | awk '/^temp1:/{gsub("\\+|°C",""); print $2; exit} END{if(NR==0) print "NA"}')"
fi

# 1-minute load average
LOAD1="$(awk '{print $1}' /proc/loadavg)"

# Memory used (MiB)
MEM_USED_MB="$(free -m | awk '/^Mem:/{print $3}')"

# Root filesystem usage (%)
DISK_USE_PCT="$(df -P / | awk 'NR==2{gsub("%","",$5); print $5}')"

# GPU/SoC throttling flags (optional; "NA" if not available)
if command -v vcgencmd >/dev/null 2>&1; then
  THROTTLED="$(vcgencmd get_throttled 2>/dev/null | awk -F= '{print $2}')"
else
  THROTTLED="NA"
fi

# Prepare log directory
sudo mkdir -p "$(dirname "$LOG_FILE")"
if [[ ! -s "$LOG_FILE" ]]; then
  echo "timestamp,cpu_temp_c,load1,mem_used_mb,disk_root_use_pct,throttled" | sudo tee "$LOG_FILE" >/dev/null
fi

echo "$TIMESTAMP,$CPU_TEMP_C,$LOAD1,$MEM_USED_MB,$DISK_USE_PCT,$THROTTLED" > "$TMPFILE"
sudo tee -a "$LOG_FILE" < "$TMPFILE" >/dev/null
rm -f "$TMPFILE"

Make it executable:

sudo install -m 0755 pi_telemetry.sh /usr/local/bin/

Let it run once to create the CSV:

/usr/local/bin/pi_telemetry.sh && tail -n3 /var/log/pi_metrics.csv

3) Add a tiny on-device anomaly detector (Isolation Forest)

This script learns a baseline from recent rows and classifies the newest row as normal or anomalous. It’s small, fast, and works well for mixed metrics.

Create /usr/local/bin/anomaly_iforest.py:

#!/usr/bin/env python3
import csv, sys, os, json
from sklearn.ensemble import IsolationForest
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import make_pipeline
import numpy as np

path = sys.argv[1] if len(sys.argv) > 1 else "/var/log/pi_metrics.csv"
window = int(os.environ.get("PI_IFOREST_WINDOW", "500"))     # training window
contam = float(os.environ.get("PI_IFOREST_CONTAM", "0.02"))  # expected anomaly ratio

rows = []
with open(path, newline="") as f:
    reader = csv.DictReader(f)
    for r in reader:
        try:
            # Use numeric features only
            feats = [
                float(r["cpu_temp_c"]) if r["cpu_temp_c"] != "NA" else np.nan,
                float(r["load1"]),
                float(r["mem_used_mb"]),
                float(r["disk_root_use_pct"]),
            ]
            # Replace NaN with column means later
            rows.append((r["timestamp"], feats))
        except Exception:
            continue

if len(rows) < 30:
    print(json.dumps({"status": "insufficient_data", "count": len(rows)}))
    sys.exit(0)

# Take last N rows
rows = rows[-window:] if len(rows) > window else rows

timestamps = [r[0] for r in rows]
X = np.array([r[1] for r in rows], dtype=float)

# Simple NaN imputation with column means
col_means = np.nanmean(X, axis=0)
inds = np.where(np.isnan(X))
X[inds] = np.take(col_means, inds[1])

# Train on all but the last row; score the last row
if X.shape[0] < 10:
    print(json.dumps({"status": "insufficient_data", "count": X.shape[0]}))
    sys.exit(0)

X_train = X[:-1]
x_test = X[-1].reshape(1, -1)
ts_test = timestamps[-1]

model = make_pipeline(
    StandardScaler(),
    IsolationForest(
        n_estimators=128,
        contamination=contam,
        random_state=42,
        n_jobs=1,
        warm_start=False
    )
)
model.fit(X_train)
pred = model.predict(x_test)[0]  # 1 normal, -1 anomaly
score = model.score_samples(x_test)[0]  # lower = more anomalous

out = {
    "status": "ok",
    "timestamp": ts_test,
    "anomaly": bool(pred == -1),
    "iforest_score": float(score),
    "features": {
        "cpu_temp_c": float(X[-1,0]),
        "load1": float(X[-1,1]),
        "mem_used_mb": float(X[-1,2]),
        "disk_root_use_pct": float(X[-1,3]),
    }
}
print(json.dumps(out))

Make it executable:

sudo install -m 0755 anomaly_iforest.py /usr/local/bin/

Try it:

/usr/local/bin/anomaly_iforest.py /var/log/pi_metrics.csv | jq .

Tuning tips:

  • PI_IFOREST_WINDOW controls how much recent history defines “normal” (e.g., 200–2000 rows).

  • PI_IFOREST_CONTAM sets sensitivity (0.01–0.05 are common; higher = more sensitive).

4) Automate and alert on anomalies

Create /usr/local/bin/monitor.sh to collect, score, and alert:

#!/usr/bin/env bash
set -euo pipefail

LOG_JSON="/tmp/pi_anomaly.json"
WEBHOOK_URL="${WEBHOOK_URL:-}"   # set this to your Slack/Discord/Mattermost webhook
WINDOW="${PI_IFOREST_WINDOW:-500}"
CONTAM="${PI_IFOREST_CONTAM:-0.02}"

# 1) Collect a new row
/usr/local/bin/pi_telemetry.sh

# 2) Score the latest row
PI_IFOREST_WINDOW="$WINDOW" PI_IFOREST_CONTAM="$CONTAM" \
  /usr/local/bin/anomaly_iforest.py /var/log/pi_metrics.csv > "$LOG_JSON" || {
    logger -t pi-monitor "anomaly script failed"
    exit 1
  }

ANOMALY=$(jq -r '.anomaly // empty' "$LOG_JSON" || true)
STATUS=$(jq -r '.status // empty' "$LOG_JSON" || true)

if [[ "$STATUS" != "ok" ]]; then
  logger -t pi-monitor "Status=$STATUS (waiting for more data)"
  exit 0
fi

if [[ "$ANOMALY" == "true" ]]; then
  TEXT="$(jq -r '
    "Raspberry Pi anomaly @ " + .timestamp
    + "\n  temp: " + (.features.cpu_temp_c|tostring) + " C"
    + "\n  load1: " + (.features.load1|tostring)
    + "\n  mem_used_mb: " + (.features.mem_used_mb|tostring)
    + "\n  disk%: " + (.features.disk_root_use_pct|tostring)
    + "\n  iforest_score: " + (.iforest_score|tostring)
  ' "$LOG_JSON")"

  logger -t pi-monitor "$TEXT"

  if [[ -n "${WEBHOOK_URL}" ]]; then
    PAYLOAD=$(jq -n --arg t "$TEXT" '{text:$t}')
    curl -sS -X POST -H "Content-type: application/json" \
      --data "$PAYLOAD" "$WEBHOOK_URL" >/dev/null || true
  fi
fi

Install and schedule it every minute with cron:

sudo install -m 0755 monitor.sh /usr/local/bin/
sudo crontab -e
# Add this line (runs every minute):
*/1 * * * * WEBHOOK_URL="https://hooks.slack.com/services/XXX/YYY/ZZZ" /usr/local/bin/monitor.sh

No webhook? The script still logs anomalies to syslog:

sudo journalctl -t pi-monitor -f

5) Real-world checks and examples

  • Overheating and throttling

    • Reboot, let the model learn a baseline for 30+ minutes.
    • Stress the CPU:
    stress-ng --cpu 4 --timeout 120
    
    • Expect a spike in temperature and load; the model should alert on the deviation.
    • If vcgencmd is available, you’ll see throttling flags change from 0x0 when throttling occurs.
  • Runaway process or memory leak

    • Start a memory hog or runaway Python script to grow memory usage; the detector should trigger as mem_used_mb drifts.
  • Disk filling fast

    • Copy a large file to / to force disk usage up quickly; anomalies fire because disk_root_use_pct deviates from baseline.
  • Nightly cron storms

    • If 3 AM backups briefly heat and load the Pi every day, the model learns that as “normal” for that time window; truly novel spikes still alert.

Why a tiny AI model is valid here

  • It’s contextual: Instead of naïve thresholds, it learns your Pi’s “normal” across temperature, load, memory, and disk. Nightly spikes don’t spam you—genuine deviations do.

  • It’s resource-light: Isolation Forest with a few hundred samples runs in milliseconds and uses little RAM/CPU.

  • It’s transparent: You can inspect inputs, outputs, and tune sensitivity without black boxes.

Next steps (CTA)

  • Add more signals: network errors, service restarts, journal anomalies, SD card wear indicators (e.g., mmc errors from dmesg).

  • Persist models between runs for faster scoring, or maintain day/night baselines.

  • Ship metrics to InfluxDB/Prometheus and use this anomaly flag as an alert channel.

  • Harden it: convert cron to a systemd timer, add log rotation for /var/log/pi_metrics.csv, and unit tests for your scripts.

If this helped, wire it into your Pi today, trigger a safe test anomaly with stress-ng, and tune PI_IFOREST_CONTAM until alerts feel “just right.” Your future self (and your SD card) will thank you.