This project provides a lightweight, self-hosted CCTV solution designed for Linux-based single-board computers (SBCs) and standard USB webcams. It offers an affordable, privacy-focused alternative for home monitoring by keeping your video data entirely under your control. Project Philosophy
- Privacy-First: By storing all footage locally, this system eliminates the need for third-party cloud subscriptions and ensures your data never leaves your network.
- Cost-Effective: Leverage existing hardware—such as a spare Linux board and a USB webcam—to build a fully functional surveillance system without recurring fees.
- Minimalist Architecture: The software is optimized to run efficiently on low-power devices, ensuring high performance even on entry-level hardware.
Key Features
- Hardware Agnostic: Highly compatible with a wide range of standard USB webcams.
- Resource Efficient: Optimized specifically for Linux-based boards (e.g., Raspberry Pi, Orange Pi, or similar SBCs).
- Data Sovereignty: Full control over your storage path, retention policies, and access methods.
- Simple Deployment: Designed for quick setup and easy maintenance.
- Live MJPEG stream with a web dashboard
- Motion detection with frame differencing
- Automatic recording with a pre-motion buffer
- Email alerts with a snapshot picture when motion starts
- Telegram integration:
- Automatic video upload after motion is recorded
- Bot commands:
/snapshot,/video <seconds>,/help
- Night mode low-light enhancement (software CLAHE + brightness/contrast boost)
- Recordings bulk actions: select all, send to Telegram, download ZIP, delete
- Storage cleanup by age, total size, and emergency low-disk cleanup
- systemd autostart ready
- Licensed under GNU AGPLv3
- Python 3.10 or newer
- OpenCV with V4L2 support (see installation options below)
- A USB webcam (
/dev/video0by default) - Optional: a Telegram bot token for Telegram notifications
- Optional: SMTP credentials for email alerts
-
Verify OpenCV is available:
python3 -c "import cv2; print(cv2.__version__)"If OpenCV is not installed, you have two options:
- Option A — use a system OpenCV package (recommended on ARM boards such as the Odroid XU4, where a pre-built system package is usually optimized for the board).
- Option B — install OpenCV via pip (convenient on x86/amd64 machines or when no system package is available). Use
opencv-python-headlessbecause the app does not need a GUI.
-
Create and activate the virtual environment and install the app:
This creates a venv that can see the system site-packages, so the system
cv2is available inside the venv.cd $HOME/CheapSecurity python3 -m venv venv --system-site-packages source venv/bin/activate pip install -e .
Use this if you do not have a system OpenCV or prefer a self-contained venv.
cd $HOME/CheapSecurity python3 -m venv venv source venv/bin/activate pip install opencv-python-headless pip install -e .
Note: On some older ARM boards the pip
opencv-python-headlesswheel may not be available or may be slow. If that happens, install OpenCV from your distribution’s package manager instead and use Option A. -
Find your webcam device (usually
/dev/video0): -
Copy
config.json.exampletoconfig.jsonand edit it:cp config.json.example config.json nano config.json
- Set camera device, resolution, and frame rate
- Fill in SMTP credentials if you want email alerts
- Fill in Telegram bot token and chat ID if you want Telegram uploads
Security:
config.jsonis listed in.gitignoreand must never be committed. It contains passwords and tokens. Always editconfig.json, notconfig.json.example. If you add a new setting, update both files so the example stays in sync. -
Run the app:
./venv/bin/python -m cheapsecurity.app
-
Open the dashboard in a browser:
Edit config.json:
| Section | Key | Description |
|---|---|---|
camera |
device |
V4L2 device index (0 = /dev/video0) |
camera |
width, height, fps |
Capture resolution and frame rate |
camera |
night_mode |
Enable low-light enhancement |
camera |
night_mode_fps |
Target FPS in night mode (camera may ignore this) |
camera |
night_mode_gain |
Target analog gain in night mode (camera may ignore this) |
camera |
night_mode_brightness |
Brightness boost in night mode |
camera |
night_mode_contrast |
Contrast boost in night mode |
motion |
threshold |
Pixel difference threshold (0-255) |
motion |
min_area |
Minimum contour area to trigger motion (full-res pixels) |
motion |
cooldown_seconds |
Keep recording after motion stops |
motion |
scale |
Downscale factor for motion detection (saves CPU) |
recording |
dir |
Where videos are saved |
recording |
max_duration_seconds |
Maximum length of one clip |
recording |
pre_buffer_seconds |
Seconds before motion included in clip |
recording |
codec |
Preferred FourCC codec (MJPG for low CPU, mp4v for smaller files) |
notifications |
enabled |
Send email alerts on motion |
notifications |
smtp |
SMTP server, port, username, password, TLS |
notifications |
from, to, subject |
Email sender/recipients/subject (use ["a@...", "b@..."] for multiple recipients) |
notifications |
min_interval_minutes |
Minimum time between alert emails |
telegram |
enabled |
Send videos to Telegram after motion is recorded |
telegram |
bot_token, chat_id |
Telegram Bot API token and destination chat |
telegram |
send_video |
Whether to upload the video file automatically |
telegram |
min_interval_minutes |
Minimum time between Telegram uploads |
telegram |
poll_commands |
Enable /snapshot, /video, and /help bot commands |
storage |
max_age_days |
Delete recordings older than this (default 3 days = 72h) |
storage |
max_size_gb |
Delete oldest files if total exceeds this |
storage |
cleanup_interval_minutes |
How often storage cleanup runs |
storage |
delete_old_on_startup |
If false, old recordings are kept when the app restarts |
storage |
emergency_free_space_gb |
If free disk space drops below this, delete old recordings before a new one |
storage |
emergency_delete_count |
How many oldest recordings to delete in an emergency cleanup |
web |
host, port |
Dashboard bind address and port |
web |
stream_scale |
Downscale factor for live stream (saves bandwidth/CPU) |
web.auth |
enabled, username, password |
Optional HTTP Basic Auth |
- Open Telegram and message @BotFather.
- Send
/newbotand follow the prompts to choose a display name and username. - Copy the bot token (looks like
123456789:ABCdefGHIjklMNOpqrsTUVwxyz). - Keep this token secret — anyone with it can control your bot.
- Start a private chat with your new bot and send any message (for example,
/start). - Open this URL in a browser, replacing
<YOUR_BOT_TOKEN>with the real token:https://api.telegram.org/bot<YOUR_BOT_TOKEN>/getUpdates - Look for
"chat":{"id":123456789. The number is your chat ID.- If
getUpdatesis empty, send another message to the bot and refresh. - If you want to use a group chat, add the bot to the group first and send a message there; the chat ID will be negative for groups.
- If
- Copy the chat ID exactly, including the
-sign if it is a group.
Fill in the telegram section of config.json:
"telegram": {
"enabled": true,
"bot_token": "123456789:ABCdefGHIjklMNOpqrsTUVwxyz",
"chat_id": "123456789",
"send_video": true,
"min_interval_minutes": 5,
"poll_commands": true
}Then restart the service:
sudo systemctl restart cheapsecurity@$(whoami).serviceAfter a motion clip is saved, the video is uploaded to your Telegram chat. Uploads are rate-limited by min_interval_minutes.
From your configured chat, send:
/snapshot— receive the current camera picture/video 10— record and send a 10-second video (1–60 seconds, default 10)/help— list commands
The bot only responds to your configured chat_id.
Motion has priority: if the system is already recording because motion was detected, a /video request will not interrupt it. The bot will reply that a motion video is in progress and will be uploaded automatically.
Configure the notifications section in config.json. A picture from the moment motion starts is attached. Alerts are rate-limited by min_interval_minutes.
Google no longer allows "less secure apps" to use your regular Gmail password. You must create an App Password.
- Enable 2-Step Verification on your Google account:
- Create an App Password:
- Go to https://myaccount.google.com/apppasswords
- Select app: Mail
- Select device: Other (Custom name) — type "CheapSecurity"
- Click Generate and copy the 16-character password (for example,
abcd efgh ijkl mnop).
- In
config.json, set:"notifications": { "enabled": true, "smtp": { "server": "smtp.gmail.com", "port": 465, "username": "[email protected]", "password": "abcdefghijklmnop", "use_tls": true }, "from": "[email protected]", "to": "[email protected]", "subject": "CheapSecurity motion alert", "min_interval_minutes": 5 }
- Use the App Password (no spaces) in the
passwordfield, not your Google account password. - For Google Workspace accounts, the username is usually your full email address.
- The app uses implicit TLS (
SMTP_SSL) on the port you configure. Gmail accepts this on port 465.
- Use the App Password (no spaces) in the
"to": [
"[email protected]",
"[email protected]"
]Night mode combines:
- Software enhancement (CLAHE on the L channel)
- Camera brightness/contrast boost
- Attempts to lower FPS and raise gain/ISO if the camera supports it
Toggle it from the dashboard. It is applied to the live stream, recordings, and alert pictures.
Important: most USB webcams do not expose ISO/gain/exposure controls via V4L2, so FPS/gain adjustments may be ignored. True night vision requires an IR-sensitive camera and an IR illuminator.
- Recordings are saved in
recordings/. - Recordings older than
max_age_daysare deleted during periodic cleanup, not on startup (unlessdelete_old_on_startupistrue). - If free disk space drops below
emergency_free_space_gb, the oldestemergency_delete_countrecordings are deleted before starting a new clip. - Recordings older than
max_age_daysor exceedingmax_size_gbare removed during periodic cleanup.
- Live stream
- Status panel (resolution, FPS, recording state, motion state)
- Settings toggles: night mode, email notifications, Telegram uploads, built-in basic auth
- Recordings list with per-row checkboxes and bulk actions:
- Select all
- Send to Telegram
- Download selected (ZIP)
- Delete selected
Do not expose Flask's development server to the internet. Use Gunicorn behind the built-in auth or another reverse proxy you trust.
It is already defined in pyproject.toml:
cd $HOME/CheapSecurity
source venv/bin/activate
pip install -e .Copy the service template and enable it from a user shell:
sudo cp cheapsecurity.service /etc/systemd/system/[email protected]
sudo systemctl daemon-reload
sudo systemctl enable --now cheapsecurity@$(whoami).serviceThis binds Gunicorn to 0.0.0.0:5000 with one worker and four threads, so the dashboard and stream are reachable directly on your network. Only one worker is used because the camera must be opened by a single process.
Security: if you expose this to the internet, put a reverse proxy with HTTPS and authentication in front of Gunicorn. If you only access it locally, keep the built-in auth enabled.
View logs:
sudo journalctl -u cheapsecurity@$(whoami).service -fCheapSecurity/
├── src/
│ └── cheapsecurity/ # Python package
│ ├── app.py # Development launcher
│ ├── cctv.py # Motion detection, recording, alerts, Telegram bot
│ ├── web.py # Flask dashboard and APIs
│ ├── wsgi.py # Production WSGI entry point
│ ├── diagnose.py # Diagnostic/troubleshooting script
│ ├── templates/ # HTML templates
│ └── static/ # CSS/JS
├── tests/ # Test suite
├── config.json # Your local settings (gitignored, never commit)
├── config.json.example # Example settings template (committed)
├── pyproject.toml # Package metadata and dependencies
├── cheapsecurity.service # systemd template
├── LICENSE # GNU AGPLv3
└── recordings/ # Saved videos
If recordings stop appearing:
- Check the service is running:
sudo systemctl status cheapsecurity@$(whoami).service - Check logs:
sudo journalctl -u cheapsecurity@$(whoami).service -f - Run the diagnostic script:
source venv/bin/activate python -m cheapsecurity.diagnose - Try lowering
motion.min_areaif no motion is detected.
This project is licensed under the GNU Affero General Public License v3.0 or later (AGPLv3). See LICENSE.