In my previous post I connected a Raspberry Pi 5 to my AI assistant as a remote node. The natural next step: attach a camera and turn it into a proper garden security system — motion-activated, AI-powered, with notifications straight to Telegram.

This post documents what I built: Frigate NVR running on the Pi 5, a Freenove 8MP camera module, automated clip retention, and a Python bot that sends snapshots to a private Telegram channel the moment something moves.

Hardware

  • Raspberry Pi 5 8GB — already running as an OpenClaw node
  • Freenove 8MP Camera Module (IMX219 sensor, 120° FOV) — £14.95 on Amazon
  • MicroSD card — 32GB for now; planning to add a USB-C SSD to avoid card wear from continuous writes

Total hardware cost: under £15 extra, since the Pi was already set up.

Architecture

Camera (IMX219)
    ↓ rpicam-vid (libav/RTSP)
mediamtx (RTSP relay, :8556)
    ↓
Frigate NVR (AI detection, recording, web UI)
    ↓ MQTT events
Mosquitto broker
    ↓
frigate-notify.py (Python bot)
    ↓
Telegram → 📷 Garden Camera topic

I chose Frigate over simpler motion-detection tools (like motion or MotionEyeOS) because it does proper AI object detection — it can tell the difference between a person, a cat, and a tree branch blowing in the wind. Far fewer false alarms.

Step 1: Connect the Camera

The Freenove camera uses a CSI ribbon cable. The Pi 5 has two CSI ports — labelled CAM0 and CAM1. I connected mine to CAM1 (the slot closer to the USB ports).

The ribbon cable itself is black on one side and has a silver/blue contact strip on the other. Getting this right is straightforward — but which slot you use matters for the next step.

Test it works:

rpicam-hello --timeout 5000

A preview window should appear for 5 seconds. If you get no cameras available, the most likely culprit is the wrong slot without the matching dtoverlay — not the cable orientation.

Note: rpicam-apps was already installed on my Pi (Debian Trixie). If not: sudo apt install rpicam-apps

Step 1a: Enable the Camera in config.txt

The Pi 5 doesn’t auto-detect CSI cameras without a device tree overlay. Following the Freenove getting started guide, add this to /boot/firmware/config.txt:

dtoverlay=imx219,cam1

The cam1 suffix tells the kernel to look for the IMX219 sensor on CSI port 1. If you’re using CAM0, use cam0 instead — this is the bit that actually matters. Without the correct overlay, rpicam-hello will return no cameras available regardless of how perfectly the cable is seated.

After editing, reboot:

sudo reboot

Then test again with rpicam-hello --timeout 5000.

Step 2: Install Docker

curl -fsSL https://get.docker.com | sudo sh
sudo usermod -aG docker YOUR_USER

Log out and back in for the group change to take effect.

Why Docker? The Frigate stack involves three separate services — Frigate itself, an MQTT broker (Mosquitto), and an RTSP relay (mediamtx). Managing these as native packages would mean juggling different init scripts, config paths, and dependency conflicts. Docker lets you declare the whole stack in one docker-compose.yml, start everything with a single command, and upgrade or roll back any component independently. Frigate also publishes official Docker images with all its Python and ML dependencies pre-bundled — installing it natively on a Pi is a multi-hour dependency hell.

The Pi 5 with 8GB RAM handles three containers comfortably alongside everything else.

Step 3: Set Up the Directory Structure

mkdir -p ~/frigate/config ~/frigate/storage
mkdir -p ~/frigate/mosquitto/config ~/frigate/mosquitto/data ~/frigate/mosquitto/log

Step 4: Mosquitto Config

~/frigate/mosquitto/config/mosquitto.conf:

listener 1883
allow_anonymous true
persistence true
persistence_location /mosquitto/data/
log_dest file /mosquitto/log/mosquitto.log

Step 5: Frigate Config

~/frigate/config/config.yml:

mqtt:
  enabled: true
  host: 127.0.0.1
  port: 1883

cameras:
  garden:
    ffmpeg:
      inputs:
        - path: rtsp://127.0.0.1:8556/garden
          roles:
            - detect
            - record
    detect:
      enabled: true
      width: 1280
      height: 720
      fps: 5
    record:
      enabled: true
      retain:
        days: 7
        mode: motion
      events:
        retain:
          default: 14
    motion:
      threshold: 25
      contour_area: 100
    objects:
      track:
        - person
        - cat
        - dog
        - bird
        - car

detectors:
  cpu:
    type: cpu
    num_threads: 4

A few things worth noting:

  • 5fps for detection — plenty for a garden camera, and much kinder on the Pi’s CPU than 15–30fps
  • mode: motion — only records when something is actually moving (not 24/7)
  • 7-day clip retention, 14-day event retention — enough history without filling the card
  • CPU detector — no hardware accelerator yet; the Pi 5 handles it fine at this resolution

Why 1280×720 and not 8MP?

The Freenove camera is rated 8MP (3280×2464 native resolution). So why am I streaming at 1280×720?

For AI detection, resolution is largely a waste. Frigate’s object detection model (MobileNet SSD) works on small input frames — typically 300×300 pixels internally. Feeding it a 3280×2464 stream means the CPU has to decode, resize, and process a huge image just to feed it into a tiny neural net. The accuracy gain is negligible; the CPU cost is enormous.

For recording, I’m streaming at 1280×720 from rpicam-vid but Frigate records the full stream it receives. If I wanted archival-quality footage I could increase this — but for a garden camera, 720p is perfectly readable and keeps file sizes sane.

The 15fps streaming rate from rpicam-vid vs 5fps detection in Frigate is also deliberate: Frigate sub-samples its input, so recording stays smooth while detection stays lightweight.

If I add the Hailo-8L hardware accelerator later, I can bump both stream resolution and detection fps significantly without impacting CPU.

Step 6: docker-compose

~/frigate/docker-compose.yml:

services:
  mosquitto:
    image: eclipse-mosquitto:latest
    container_name: mosquitto
    restart: unless-stopped
    ports:
      - "1883:1883"
    volumes:
      - ./mosquitto/config:/mosquitto/config
      - ./mosquitto/data:/mosquitto/data
      - ./mosquitto/log:/mosquitto/log

  mediamtx:
    image: bluenviron/mediamtx:latest
    container_name: mediamtx
    restart: unless-stopped
    network_mode: host
    environment:
      - MTX_RTSPADDRESS=:8556

  frigate:
    image: ghcr.io/blakeblackshear/frigate:stable
    container_name: frigate
    restart: unless-stopped
    privileged: true
    shm_size: "256mb"
    network_mode: host
    depends_on:
      - mosquitto
      - mediamtx
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - ./config:/config
      - ./storage:/media/frigate
      - type: tmpfs
        target: /tmp/cache
        tmpfs:
          size: 1000000000
    environment:
      FRIGATE_RTSP_PASSWORD: ""

Why mediamtx? The Pi 5 camera stack uses libcamera, which Frigate can’t read directly. mediamtx acts as an RTSP relay: rpicam-vid pushes a stream to it, and Frigate reads from it. I put mediamtx on port 8556 because Frigate’s internal go2rtc component owns 8554.

Step 7: Camera Streaming Service

Rather than a script that needs managing, I created a systemd service that keeps the camera stream alive permanently:

/etc/systemd/system/garden-camera.service:

[Unit]
Description=Garden Camera RTSP Stream
After=network.target docker.service
Requires=docker.service

[Service]
ExecStartPre=/bin/sleep 3
ExecStart=rpicam-vid -t 0 --width 1280 --height 720 --framerate 15 \
  --nopreview --codec libav --libav-format rtsp \
  --libav-video-codec libx264 \
  --libav-video-codec-opts "preset=ultrafast" \
  -o rtsp://127.0.0.1:8556/garden
Restart=always
RestartSec=5
User=YOUR_USER

[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable garden-camera.service
sudo systemctl start garden-camera.service

Step 8: Start Everything

cd ~/frigate
docker compose up -d

Check Frigate is up:

curl http://127.0.0.1:5000/api/version
curl http://127.0.0.1:5000/api/stats

The web UI is available at http://YOUR_PI_IP:5000.

Step 9: Telegram Notifications

Frigate emits events to MQTT whenever it detects something. It saves two things per event: a JPEG snapshot (the best still frame from the detection) and an MP4 video clip of the full event. The Telegram bot fetches the snapshot — fast to send, instant to view. The full clip stays on disk and is accessible via the Frigate web UI.

I wrote a small Python bot to listen to the MQTT event stream and forward snapshots to Telegram.

Install the dependency:

pip3 install paho-mqtt requests --break-system-packages

~/frigate-notify.py:

#!/usr/bin/env python3
"""Frigate → Telegram notification bot"""

import json
import time
import requests
import paho.mqtt.client as mqtt

BOT_TOKEN = "YOUR_BOT_TOKEN"
CHAT_ID = "YOUR_CHAT_ID"
THREAD_ID = YOUR_TOPIC_ID  # Forum topic ID (omit if not using topics)
FRIGATE_URL = "http://127.0.0.1:5000"
MQTT_HOST = "127.0.0.1"
MQTT_PORT = 1883

COOLDOWN = 60  # seconds between alerts for the same label
last_sent = {}

EMOJI = {
    "person": "🚶",
    "car": "🚗",
    "cat": "🐱",
    "dog": "🐶",
    "bird": "🐦",
}


def send_photo(photo_bytes, caption):
    url = f"https://api.telegram.org/bot{BOT_TOKEN}/sendPhoto"
    requests.post(url, data={
        "chat_id": CHAT_ID,
        "message_thread_id": THREAD_ID,
        "caption": caption,
    }, files={"photo": ("snapshot.jpg", photo_bytes, "image/jpeg")}, timeout=15)


def send_text(text):
    url = f"https://api.telegram.org/bot{BOT_TOKEN}/sendMessage"
    requests.post(url, json={
        "chat_id": CHAT_ID,
        "message_thread_id": THREAD_ID,
        "text": text,
    }, timeout=10)


def on_message(client, userdata, msg):
    try:
        payload = json.loads(msg.payload.decode())
    except Exception:
        return

    if msg.topic != "frigate/events":
        return

    if payload.get("type") not in ("new", "update"):
        return

    after = payload.get("after", {})
    label = after.get("label", "unknown")
    score = after.get("top_score", 0)
    event_id = after.get("id", "")

    if score < 0.6:
        return

    now = time.time()
    if label in last_sent and now - last_sent[label] < COOLDOWN:
        return
    last_sent[label] = now

    emoji = EMOJI.get(label, "📹")
    caption = f"{emoji} {label.capitalize()} detected ({score:.0%} confidence)"

    snap_url = f"{FRIGATE_URL}/api/events/{event_id}/snapshot.jpg?quality=85"
    try:
        r = requests.get(snap_url, timeout=10)
        if r.ok:
            send_photo(r.content, caption)
        else:
            send_text(caption)
    except Exception:
        send_text(caption)


def on_connect(client, userdata, flags, rc, properties=None):
    if rc == 0:
        client.subscribe("frigate/events")


def main():
    client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2)
    client.on_connect = on_connect
    client.on_message = on_message
    while True:
        try:
            client.connect(MQTT_HOST, MQTT_PORT, 60)
            client.loop_forever()
        except Exception as e:
            print(f"MQTT error: {e}, retrying...")
            time.sleep(10)


if __name__ == "__main__":
    main()

Run it as a systemd service:

/etc/systemd/system/frigate-notify.service:

[Unit]
Description=Frigate Telegram Notification Bot
After=network.target docker.service
Requires=docker.service

[Service]
ExecStart=/usr/bin/python3 /home/YOUR_USER/frigate-notify.py
Restart=always
RestartSec=10
User=YOUR_USER

[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable frigate-notify.service
sudo systemctl start frigate-notify.service

Pitfalls

A few things that caught me out:

Wrong CSI slot without matching dtoverlay. I connected the camera to CAM1 but the default overlay targets CAM0. The fix is simple — dtoverlay=imx219,cam1 in /boot/firmware/config.txt — but it’s easy to miss if you follow a guide written for a single-port Pi.

Camera port conflict. Frigate’s internal go2rtc wants port 8554 for RTSP. If you put mediamtx on 8554 too, go2rtc can’t bind and throws errors. Solution: move mediamtx to 8556 via MTX_RTSPADDRESS=:8556.

rpicam-vid option syntax. The flag is --framerate, not -r. The codec option separator for --libav-video-codec-opts uses = and : — check rpicam-vid --help if you hit unknown option errors.

brace expansion in remote exec. Running mkdir -p {config,storage} via a non-interactive shell doesn’t expand braces. Use separate arguments instead.

mediamtx config format. The YAML schema changes between versions. When in doubt, run with no config at all — the defaults work fine for this use case.

SD card wear. Continuous motion-triggered writes will wear out a microSD card over months. Mount a USB-C SSD and point Frigate’s storage there — it’s a one-line change in docker-compose.yml:

- /mnt/ssd/frigate:/media/frigate

Resource Usage

Here’s what the stack actually costs on the Pi 5 8GB, measured a few hours after bringing everything up:

ProcessCPURAM
frigate.detector:cpu (AI inference)~19%166 MB
rpicam-vid (camera + H.264 encoding)~18%132 MB
frigate.process:garden (frame processing)~2%158 MB
frigate main process~1%341 MB
frigate.capture:garden~1%145 MB
mediamtx~0.7%45 MB
Total system~81% CPU1.8 GB RAM

CPU: ~81% total — higher than expected, largely driven by the CPU doing software H.264 encoding in rpicam-vid (~18%) alongside Frigate’s AI inference (~19%) and frame processing. The Pi 5 has 4 cores, so this is roughly 3 of 4 cores busy. It’s functional but leaves limited headroom for anything else.

Temperature: 66°C — within safe range (Pi 5 throttles at 85°C), but warm enough that a heatsink or case with active cooling is worthwhile for permanent outdoor deployment.

RAM: 1.8 GB used out of 7.9 GB — no pressure at all. Swap is untouched.

The CPU load is the thing to watch. Two levers to pull it down significantly:

  1. Hailo-8L M.2 HAT (~£50-70) — offloads AI inference to dedicated hardware, dropping the detector’s ~19% CPU share to near zero
  2. Hardware H.264 encoding — the Pi 5’s V4L2 M2M encoder can replace libx264 in software, saving another ~15-18%

For a single camera with nothing else running on the Pi, it works. For multiple cameras or a busier system, the HAT becomes important.

Result

The Frigate web UI shows a live camera feed, event timeline, and lets you replay clips. When something moves in the garden, a snapshot arrives in Telegram within a couple of seconds — label, confidence score, and a still frame from the moment of detection.

Next step: add the Hailo-8L M.2 HAT for hardware-accelerated inference — Frigate supports it natively and it cuts CPU usage dramatically. Also planning to mount a USB-C SSD for recording storage to stop wearing out the microSD card.

Total time from unboxing to working CCTV: about two hours, mostly debugging the RTSP transport chain between rpicam-vid, mediamtx, and Frigate.