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. CAMMY_ADMIN_USER and CAMMY_ADMIN_PASSWORD are
required when no users exist. Development environments may explicitly set
CAMMY_ALLOW_DEFAULT_ADMIN=true to create admin / cammy-cam with a forced
password reset.
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/auth/reset-request |
Queue a 15-minute reset token for out-of-band delivery. Always returns a generic response. |
POST |
/api/auth/reset-confirm |
Consume a reset token and set a new password. |
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. |
POST |
/api/event/battery |
Receive camera battery telemetry. |
POST |
/api/event/detection |
Persist bird, person, car, or other detector output. |
GET |
/api/activity |
Search persisted motion and detections. |
POST |
/graphql |
Authenticated GraphQL activity queries. |
POST |
/mcp/ |
API-key-protected MCP Streamable HTTP activity tools. |
GET |
/api/cameras/{id}/battery |
Read latest battery telemetry. |
GET/PUT |
/api/cameras/{id}/motion-zones |
Read or replace normalized smart-motion zones. |
POST |
/api/firmware?version=...&kind=ota|factory |
Stage an OTA app or merged factory image (admin). |
GET |
/api/firmware/manifest |
ESP Web Tools install manifest. |
POST |
/api/devices/{id}/ota |
Trigger device OTA installation. |
POST |
/api/devices/{id}/ota/rollback |
Trigger previous-partition rollback. |
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.
Production startup requires CAMMY_ADMIN_USER and CAMMY_ADMIN_PASSWORD before
the first user is created. CAMMY_ALLOW_DEFAULT_ADMIN=true enables the legacy
admin / cammy-cam credentials for development only and forces password
reset. Passwords use bcrypt; successful login transparently upgrades legacy
salted SHA-256 records. Set CAMMY_RESET_TOKEN_FILE to a protected queue file
consumed by an external notification service. Reset tokens expire after 15
minutes and never appear in API responses.