CheapSecurity

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.

Features

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

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

Requirements

Python 3.10 or newer

OpenCV with V4L2 support (see installation options below)

A USB webcam (/dev/video0 by default)

Optional: a Telegram bot token for Telegram notifications

Optional: SMTP credentials for email alerts

Quick start

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-headless because the app does not need a GUI.

Verify OpenCV is available:

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-headless because the app does not need a GUI.

Create and activate the virtual environment and install the app:

Option A: system OpenCV (recommended for ARM)

This creates a venv that can see the system site-packages, so the system cv2 is available inside the venv.

cd $HOME/CheapSecurity

python3 -m venv venv --system-site-packages

source venv/bin/activate

pip install -e .

Option B: install everything via pip

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-headless wheel may not be available or may be slow. If that happens, install OpenCV from your distribution’s package manager instead and use Option A.

Create and activate the virtual environment and install the app:

Option A: system OpenCV (recommended for ARM)

This creates a venv that can see the system site-packages, so the system cv2 is available inside the venv.

Option B: install everything via pip

Use this if you do not have a system OpenCV or prefer a self-contained venv.

Note: On some older ARM boards the pip opencv-python-headless wheel 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

Find your webcam device (usually /dev/video0):

Copy config.json.example to config.json and 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.json is listed in .gitignore and must never be committed. It contains passwords and tokens. Always edit config.json, not config.json.example. If you add a new setting, update both files so the example stays in sync.

Copy config.json.example to config.json and edit it:

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.json is listed in .gitignore and must never be committed. It contains passwords and tokens. Always edit config.json, not config.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

Run the app:

Open the dashboard in a browser:

http://<odroid-ip>:5000

Open the dashboard in a browser:

Configuration

Edit config.json:

Telegram setup

1. Create a bot

Open Telegram and message @BotFather.

@BotFather

Send /newbot and 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.

2. Get your chat ID

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 getUpdates is 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 getUpdates is 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.

Copy the chat ID exactly, including the - sign if it is a group.

3. Configure the app

Fill in the telegram section of config.json:

Then restart the service:

Automatic uploads

After a motion clip is saved, the video is uploaded to your Telegram chat. Uploads are rate-limited by min_interval_minutes.

Bot commands

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.

Email alerts

Configure the notifications section in config.json. A picture from the moment motion starts is attached. Alerts are rate-limited by min_interval_minutes.

Gmail / Google Workspace setup

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:

https://myaccount.google.com/signinoptions/two-step-verification

https://myaccount.google.com/signinoptions/two-step-verification

https://myaccount.google.com/signinoptions/two-step-verification

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).

Go to https://myaccount.google.com/apppasswords

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": "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 the password field, 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 password field, 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.

Multiple recipients

Night mode

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.

Storage and cleanup

Recordings are saved in recordings/.

Recordings older than max_age_days are deleted during periodic cleanup, not on startup (unless delete_old_on_startup is true).

If free disk space drops below emergency_free_space_gb, the oldest emergency_delete_count recordings are deleted before starting a new clip.

Recordings older than max_age_days or exceeding max_size_gb are removed during periodic cleanup.

Web interface

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

Select all

Send to Telegram

Download selected (ZIP)

Delete selected

Production deployment

Do not expose Flask's development server to the internet. Use Gunicorn behind the built-in auth or another reverse proxy you trust.

1. Install Gunicorn

It is already defined in pyproject.toml:

2. Run with systemd + Gunicorn

Copy the service template and enable it from a user shell:

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:

Project structure

Troubleshooting

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_area if no motion is detected.

License

This project is licensed under the GNU Affero General Public License v3.0 or later (AGPLv3). See LICENSE.