A lightweight, self-hosted web dashboard for Yi / Kami cameras running yi-hack custom firmware. Built to run on a Raspberry Pi next to the cameras.
Screenshots from a demo setup with synthetic camera feeds.
- Live grid: every camera at once, in the order you set with Sort (saved on the server), low-res substream in the grid and full-res when you expand a tile, audio on tap. Streams are remuxed by MediaMTX, never transcoded, so a Pi copes easily.
- Recordings: browses the motion clips the cameras already upload over FTP, with thumbnails, a player with previous/next (arrow keys, swipe), "seen" marks and download.
- Motion today: each live tile shows today's motion count and last time; tap for the list, and a time with a matching clip opens it.
- Stats: motion events per hour and camera for any day (today by default), as a stacked bar chart; show/hide cameras and browse back through the history.
- Real-time popups in the browser on every motion event (MQTT → Server-Sent Events).
- Notifications to Telegram, ntfy and/or a generic webhook, with a snapshot and then the clip; per-camera schedule, minimum gap between notifications and a quick pause.
- Camera control: on/off switch, online state, IP, WiFi signal, uptime and firmware.
- English and Italian interface, switchable per browser (EN/IT in the top bar).
- FastAPI + Jinja2 + HTMX: no Node, no build step, no database server (SQLite).
It uses what yi-hack already provides (RTSP, FTP upload of clips, MQTT motion events, CGI endpoints), so it does not record or analyse video itself. If you want object detection or continuous recording, look at Frigate, Scrypted or Shinobi instead.
- Features
- Requirements
- Quick start
- Configuration
- Security
- Development
- Project structure
- Notes
- Contributing
- License
- Cameras with yi-hack (tested with yi-hack-Allwinner and yi-hack-Allwinner-v2), with:
- RTSP enabled.
RTSP_STREAM=bothgives the low-res substream for the grid. For live audio in the browser setRTSP_AUDIO=aac: browsers can't play PCM over HLS. - FTP upload of the motion clips to a folder the dashboard can read.
- MQTT pointed at your broker (optional, but needed for motion popups, counters and notifications).
- RTSP enabled.
- Docker with Compose, on a Raspberry Pi 4/5 or any amd64/arm64 host.
- An MQTT broker (e.g. Mosquitto) and an FTP server, if you don't have them yet.
git clone https://github.com/aledv/yi-hack-cam-dashboard.git
cd yi-hack-cam-dashboard
cp .env.example .env
cp config.yml.example config.yml
cp mediamtx/mediamtx.yml.example mediamtx/mediamtx.yml- Password hash and secret key:
Put the
pip install bcrypt python scripts/hash_password.py "your-password" python -c "import secrets; print(secrets.token_urlsafe(48))"
$$-escaped hash line inADMIN_PASSWORD_HASHand the random string inSECRET_KEY. Docker Compose interpolates$inside.env, so a bcrypt hash pasted as-is gets corrupted: the script prints the escaped version for you. .env: language, notification channels, MQTT broker,RECORDINGS_DIR(host folder with the clips). Every variable is explained in.env.example.config.yml: one block per camera (RTSP URLs, recordings folder, on/off/status URLs, MQTT topic). See the comments inconfig.yml.example.mediamtx/mediamtx.yml: one path per camera with the samenameas inconfig.yml, plus<name>_subfor the substream.- Start it:
Open
docker compose up -d # pulls ghcr.io/aledv/yi-hack-cam-dashboard docker compose up -d --build # or build the image locally
http://<host>:8055.
Names only. See .env.example for details.
| Variable | Purpose |
|---|---|
ADMIN_USER, ADMIN_PASSWORD_HASH, SECRET_KEY |
Single-user login (bcrypt hash, cookie signing key) |
LANGUAGE |
Default interface language: en (default) or it |
TZ |
Time zone (e.g. Europe/Rome) for notification schedules and pauses; default UTC |
TELEGRAM_BOT_TOKEN, TELEGRAM_CHAT_ID |
Telegram notifications |
NTFY_URL, NTFY_TOKEN |
ntfy notifications (token only for protected topics) |
WEBHOOK_URL |
Generic webhook: multipart POST with message and optional file |
MQTT_HOST, MQTT_PORT, MQTT_USER, MQTT_PASS |
MQTT broker for motion events |
MEDIAMTX_PUBLIC_BASE |
Base URL of MediaMTX as seen by the browser (reverse proxy / HTTPS) |
RECORDINGS_POLL_SECONDS |
Polling interval when recordings live on a network share |
RECORDINGS_DIR, APP_PORT |
Docker Compose only: clips folder on the host, published port |
CONFIG_PATH, DATA_DIR |
Only for running outside Docker (default /app/config.yml, /app/data) |
Every configured channel receives each notification: motion (with the latest clip's thumbnail) and camera turned on/off. The Notifications checkbox on the Cameras page turns them off per camera. The same page has a test button per channel and shows which channels are configured and whether MQTT is connected.
Motion notifications can be tuned on the Cameras page (popups, counters and Stats always record every event):
- Schedule per camera: weekdays and time ranges. A range like 22:00–07:00 runs
past midnight and belongs to the day it starts on. No range means all day.
Uses the container's time zone, so set
TZ. - Minimum gap per camera (default 10 min): at most one notification per camera in that window, so a busy camera doesn't flood you.
- Then send the clip: once the camera has uploaded the clip of a notified event,
it follows as a second message (Telegram
sendVideo, ntfy attachment, webhookfile). - Pause: all motion notifications off for 1 hour or until 07:00 tomorrow.
LANGUAGE sets the default. Each browser can switch with EN / IT in the top bar or on
the login page, and the choice is kept in a cookie. Notifications sent from MQTT events
use the default language. To add a language, add its code to LANGUAGES and its texts
to app/i18n.py.
The Live page loads HLS straight from MediaMTX (port 8888). On a plain LAN that works out
of the box. If you serve the dashboard over HTTPS through a reverse proxy, also proxy
MediaMTX on its own hostname and set MEDIAMTX_PUBLIC_BASE, otherwise the browser blocks
the stream as mixed content. mediamtx.yml.example uses regular (not Low-Latency) HLS
because LL-HLS needs HTTPS on MediaMTX itself to work on iPhone/iPad.
This is meant for your LAN, or for remote access through a VPN (e.g. WireGuard). Auth is deliberately simple: one user, bcrypt password, signed session cookie, no 2FA or rate limiting. Don't expose it directly to the internet. yi-hack cameras themselves often run with root and no password on the LAN.
cd app
python3 -m venv .venv && . .venv/bin/activate
pip install -r requirements.txt
CONFIG_PATH=../config.yml DATA_DIR=../data uvicorn main:app --reload --port 8055ffmpeg is only needed for thumbnails. Start MediaMTX with docker compose up -d mediamtx
to get live video while developing.
cd app && python -m unittest discover -s tests -v # tests
ruff check . && ruff format --check . # Python linter (repo root)
hadolint --config .hadolint.yaml app/Dockerfile # Dockerfile linterOr, without a local Python setup:
cd app && docker run --rm -v "$PWD:/app" -w /app python:3.12-slim \
sh -c "pip install -q -r requirements.txt && python -m unittest discover -s tests"CI (.github/workflows/image.yml) runs the linters, then the tests. On main it also
publishes a multi-arch image (linux/arm64, linux/amd64) to GHCR.
app/
main.py FastAPI routes (pages, HTMX partials, SSE, language switch)
config.py config.yml loading with hot reload
cameras.py camera HTTP helpers (on/off, status, status.json)
notifier.py Telegram / ntfy / webhook notifications
notify_schedule.py per-camera notification schedule (days, time ranges)
notify_settings.py minimum gap, clip follow-up, pause
mqtt_listener.py MQTT motion events -> popup, counters, notifications
recordings.py SQLite index of clips, thumbnails, motion events (per day for Stats)
events.py in-memory pub/sub for Server-Sent Events
auth.py single-user login
i18n.py English / Italian strings
templates/ Jinja2 templates
static/ CSS, icons, vendored htmx and hls.js
tests/ unittest suite
mediamtx/ MediaMTX example config
scripts/ password hash helper
config.ymlis reloaded automatically when it changes. Two exceptions needdocker compose restart: new MQTT topics (camera-app) and new MediaMTX paths (mediamtx).- Recordings are indexed in
data/recordings.db. Thumbnails are generated on first view, and rows for deleted files are pruned at startup. Motion events are kept for 365 days. - The Recordings page shows the last 2 days by default, 30 per page. Page 1 refreshes itself every 20 seconds.
- htmx and hls.js are bundled in
static/vendor/, so the app works through a VPN without internet access. Only Google Fonts is external, and it falls back to system fonts.
Issues and pull requests are welcome. Please run the linters and tests (see Development) before opening a pull request.
MIT, see LICENSE. Bundled third-party code: htmx 1.9.12 (0BSD) and hls.js 1.5.15 (Apache-2.0).




