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 streamwith a web dashboard
- Motion detectionwith frame differencing
- Automatic recordingwith a pre-motion buffer
- Email alertswith a snapshot picture when motion starts
- Telegram integration:- Automatic video upload after motion is recorded
-
Bot commands:
/snapshot,/video <seconds>,/help -
Night modelow-light enhancement (software CLAHE + brightness/contrast boost)
- Recordings bulk actions: select all, send to Telegram, download ZIP, delete
- Storage cleanupby age, total size, and emergency low-disk cleanup
- systemd autostartready
-
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 pipopencv-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):v4l2-ctl --list-devices - 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:
http://<odroid-ip>:5000
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 ( MJPGfor low CPU,mp4vfor 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/helpbot 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 yourchat ID.- IfgetUpdatesis 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 Verificationon 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 Generateand 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": "you@gmail.com", "password": "abcdefghijklmnop", "use_tls": true }, "from": "you@gmail.com", "to": "you@gmail.com", "subject": "CheapSecurity motion alert", "min_interval_minutes": 5 } - Use the App Password(no spaces) in thepasswordfield, 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
"to": [
"you@gmail.com",
"family@example.com"
]
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,noton 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/cheapsecurity@.service
sudo systemctl daemon-reload
sudo systemctl enable --now cheapsecurity@$(whoami).service
This 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 -f`
CheapSecurity/
├── 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.