The hub runs a FastAPI backend that manages cameras and users.
Endpoints require a bearer token obtained from /api/auth/login unless noted.
See ../communication/README.md for a diagram of how the hub fits into the system.
python-dotenv loads variables from .env automatically. On first run the hub
generates a random CAMMY_SECRET_KEY, saves it to .env and cammy.secret, and
prints the value. Once persisted, providing a different key via the environment
causes the hub to exit. Optional CAMMY_ADMIN_USER and CAMMY_ADMIN_PASSWORD
create the initial admin when no users exist. If omitted the hub defaults to an
admin account with password cammy-cam.
The hub supports local HTTPS with mDNS discovery. Certificates are generated per hostname using mkcert:
mkcert -install
CAMMY_NAME=cammy.local mkcert "$CAMMY_NAME"
mkdir -p certs
mv "$CAMMY_NAME.pem" "certs/$CAMMY_NAME.pem"
mv "$CAMMY_NAME-key.pem" "certs/$CAMMY_NAME-key.pem"
start-local.sh and start-docker.sh look for certificates under
certs/<CAMMY_NAME>.pem and will generate them automatically if they are
missing. Inside the container the working directory is /app, so paths remain
relative.
To build the hub container with the compiled UI and fresh certificates, run the Python helper:
python tools/build_docker.py
Set CAMMY_PORT in your .env file to choose the HTTPS port (default 443).
The Dockerfile exposes the port defined by the build argument HUB_PORT (also
443 by default). Run start-local.sh or the container to launch HTTPS on that
port. The startup script prints the login URL (ending in /login) so you can
sign in right away.
The optional CAMMY_TZ variable controls the time zone for token expiration
timestamps (default America/New_York).
The
container advertises _https._tcp via avahi-publish using the CAMMY_NAME
environment variable (for example cammy-92ab.local). Make sure
avahi-daemon, avahi-utils, and dbus are installed on your host (the
latter provides avahi-publish). Each container on the same network should have a
unique CAMMY_NAME to avoid mDNS collisions. The hub also performs periodic
mDNS discovery for _cammycam._tcp.local services and records the IP addresses
of ESP32 cameras that announce themselves.
| Method | Path | Description |
|---|---|---|
POST |
/api/auth/login |
Obtain a token pair (access & refresh). |
POST |
/api/users |
Provision a new user (admin or super only). |
GET |
/api/users |
List all users (admin or super only). |
GET |
/api/integrations |
List integration accounts (admin or super). |
POST |
/api/integrations |
Create an integration account. |
DELETE |
/api/integrations/{username} |
Delete an integration account. |
POST |
/api/auth/change-password |
Change the current user’s password. |
POST |
/api/auth/reset-request |
Create a password reset token. |
POST |
/api/auth/reset-confirm |
Confirm password reset with the token. |
POST |
/api/auth/refresh |
Refresh an expired access token. |
POST |
/api/auth/unlock |
Unlock a locked account using OTP. |
User accounts temporarily lock after 5 failed logins for 5 minutes. If another 5 failures occur afterwards, the account is permanently locked. Admin accounts can be recovered with an OTP generated during the first login.
Integration accounts cannot log in to the UI. They are created by admins and receive
scoped API permissions such as viewer, admin, or super. When authenticating,
they receive the same JSON response as normal users but with the longer token lifetime.
| Method | Path | Description |
|---|---|---|
POST |
/api/cameras/register |
Register or update a camera record. |
POST |
/api/key-exchange |
ECDH handshake for encrypted streams. |
GET |
/api/cameras |
List all cameras. |
GET |
/api/cameras/{camera_id} |
Get details for a camera. |
PUT |
/api/cameras/{camera_id} |
Update a camera’s info. |
DELETE |
/api/cameras/{camera_id} |
Remove a camera entry. |
GET |
/api/cameras/{camera_id}/status |
Connection and client counts. |
| Type | Path | Notes |
|---|---|---|
| WebSocket | /ws/camera/{camera_id} |
Camera connects and pushes frames. |
| WebSocket | /ws/client/{camera_id} |
Clients receive frames. |
| HTTP | /mjpeg/{camera_id} |
MJPEG fallback for browsers. |
| Method | Path | Description |
|---|---|---|
GET |
/api/speedtest |
Returns latency for a small transfer. |
GET |
/api/system-metrics |
CPU, memory, disk, network usage. |
POST |
/api/upload-clip-part |
Save a recording chunk from a camera. |
POST |
/api/upload-clip-meta |
Finalize and merge recorded chunks. |
| Method | Path | Description |
|---|---|---|
POST |
/api/event/motion |
Receive a motion detection event. |
Example payload:
{
"camera_id": "abcd1234",
"active": true
}
After logging in you can visit /api-docs for the interactive API documentation.
The underlying OpenAPI schema is available at /openapi.json and also requires authentication.
| Method | Path | Description |
|---|---|---|
POST |
/api/devices/{camera_id}/format-sd |
Format the camera SD card |
GET |
/api/devices/{camera_id}/sd-status |
Query SD card usage |
POST |
/api/devices/{camera_id}/backup-config |
Request config backup |
POST |
/api/devices/{camera_id}/reboot |
Reboot the camera |
The hub uses SQLite via SQLAlchemy. The main tables are shown below.
classDiagram
class User {
string username
string password
string salt
string role
bool must_reset
}
class Setting {
string key
string value
}
class AuthHistory {
int id
string username
bool success
datetime timestamp
}
class ConfigChange {
int id
string user
string change
datetime timestamp
}
CammyCam publishes camera status and motion events over MQTT so Home Assistant or
other services can react. Enable it by setting CAMMY_MQTT_HOST (and optional
credentials) in your .env file.
CAMMY_MQTT_HOST – Broker hostname or IPCAMMY_MQTT_PORT – Broker port (default 1883)CAMMY_MQTT_USER – Username (optional)CAMMY_MQTT_PASSWORD – Password (optional)CAMMY_MQTT_TLS – 1 to use TLS, 0 to disablecammycam/<camera_id>/status – payload {camera_id, event:"status", status, timestamp}cammycam/<camera_id>/motion – payload {camera_id, event:"motion", active, timestamp}cammycam/<camera_id>/heartbeat – periodic heartbeat messagesThe hub also publishes Home Assistant discovery messages on
homeassistant/binary_sensor/<camera_id>_motion/config and
homeassistant/binary_sensor/<camera_id>_status/config so cameras appear
automatically with binary motion and status sensors.