REST API server for Shelly Manager built with Litestar.
- HTTP REST API for device management
- OpenAPI documentation at
/docs - Request/response validation
- Health monitoring endpoint
- CORS support for web integration
- Device configuration backup & restore (encrypted snapshots)
- Docker deployment ready
# Run API server
docker run -p 8000:8000 \
ghcr.io/jfmlima/shelly-manager-api:latest
# Visit API documentation
open http://localhost:8000/docs# Install dependencies (from project root)
uv sync --package shelly-manager-api --extra dev
# Start development server
uv run --package shelly-manager-api python -m api.main
# API available at http://localhost:8000
# Docs available at http://localhost:8000/docsGET /api/health # Service health checkGET /api/devices/scan # Scan network for devices
?targets=192.168.1.0/24 # Comma-separated targets
&use_mdns=false # Use mDNS discovery
&timeout=3.0 # Timeout per device
&max_workers=50 # Concurrent workersGET /api/devices/{ip}/status # Get device status
POST /api/devices/{ip}/update # Update device firmware
# Body: {"channel": "stable", "source": "internet"}
# channel: stable (default) or beta
# source: internet (default) lets the device download from Shelly
# local serves the firmware from this manager instead
POST /api/devices/{ip}/reboot # Reboot device
POST /api/devices/bulk/update # Bulk firmware updates
# Body: {"device_ips": ["192.168.1.100", "192.168.1.101"], "channel": "stable"}
# Component Actions
GET /api/devices/{ip}/components/actions # Discover available actions
POST /api/devices/{ip}/components/{id}/action # Execute component actionBundles the manager has downloaded for local updates. Devices fetch the download route themselves and are not authenticated, so it must be reachable from the device network.
GET /api/firmware # List cached bundles
DELETE /api/firmware/{id} # Delete a bundle and its file
GET /api/firmware/{id}/download # The zip a device fetchesPer-device configuration snapshots, stored in the local database and encrypted with
SHELLY_SECRET_KEY. A backup captures every component's config (plus script code and
schedules) and is keyed by device MAC.
Backup/restore vs. bulk config: restore puts a single device's own captured config back, for recovery.
POST /api/devices/bulk/config/applydoes the opposite, pushing one config out to many devices for templating. They cover different needs, so both are kept.
GET /api/backups # List backup summaries (newest first)
?device_mac=AABBCCDDEEFF # Optional: filter by device MAC
POST /api/backups # Capture a backup of a device
# Body: {"device_ip": "192.168.1.100", "name": "before-upgrade"}
GET /api/backups/{id} # Get a backup including its full snapshot
POST /api/backups/{id}/restore # Restore a backup onto a device
# Body: {"device_ip": "192.168.1.100",
# "component_keys": ["switch:0", "sys"], # null = all except network types
# "allow_mac_mismatch": false, # restore even if target MAC differs
# "reboot": false} # reboot after a successful restore
DELETE /api/backups/{id} # Delete a backupRestore is per-component and excludes network components (wifi/eth/mqtt/ws/cloud)
by default to avoid locking the device off-network; pass their keys in component_keys to
include them.
Gen1 relay/roller/plug/i3 devices restore too, by replaying the raw /settings captured in the
snapshot over the legacy HTTP endpoints, including the device mode (relay/roller), which is
applied first and reboots the device when it differs from the backup. Caveats: secrets are
never echoed by Gen1 (GET /settings omits the WiFi STA password and mqtt_pass), so a
wifi/mqtt restore re-applies everything except the password; device auth
(/settings/login), Gen1 actions and light-device (Dimmer/Bulb/RGBW2) configs are not
restored. A backup and a target of different generations are refused. See the core README for
the full list.
Run backups automatically on a schedule with optional retention. Schedules live in the database; an in-process scheduler on the API server polls for due schedules and runs them. This relies on the API running as a single worker (the default), since one scheduler instance must own the timer. With multiple workers each one would run the scheduler and duplicate every backup.
GET /api/backup-schedules # List schedules (newest first)
POST /api/backup-schedules # Create a schedule
# Body: {"name": "nightly",
# "every": "daily", # or "interval_seconds": 21600 (exactly one)
# "target_ips": ["192.168.1.100"], # plus optional "target_macs" and
# "all_credentialed": false, # "all_credentialed" (at least one target)
# "retention_keep_last": 7, # optional retention
# "retention_max_age_days": 30}
GET /api/backup-schedules/{id} # Get one schedule
PUT /api/backup-schedules/{id} # Partial update
DELETE /api/backup-schedules/{id} # Delete
POST /api/backup-schedules/{id}/enable # Enable
POST /api/backup-schedules/{id}/disable # Disable
POST /api/backup-schedules/{id}/run # Run now, ignoring the next run timeTargets are the union of target_ips (single IPs, or ranges/CIDR that are scanned for live
devices at run time), target_macs (each resolved to an IP through the device's last-seen
address), and all_credentialed (every device with stored credentials). A MAC with no known IP
is reported as skipped, not failed. Retention only prunes scheduled snapshots, so manual backups
are never removed by a schedule. A missed run fires once on catch-up rather than backfilling.
The Component Actions system provides dynamic action discovery and execution for individual device components.
GET /api/devices/{ip}/components/actions # Get all available actions for devicePOST /api/devices/{ip}/components/{component_id}/actionFor password-protected Shelly Gen2 devices. Requires SHELLY_SECRET_KEY environment variable.
GET /api/credentials # List stored credentials (passwords hidden)
POST /api/credentials # Set/update device credentials
# Body: {"mac": "AABBCCDDEEFF", "password": "secret", "username": "admin"}
DELETE /api/credentials/{mac} # Delete stored credentialsGET /api/healthcurl "http://localhost:8000/api/devices/scan?targets=192.168.1.1-10"Response:
[
{
"ip": "192.168.1.100",
"status": "online",
"device_type": "shelly1pm",
"device_name": "Living Room Light",
"firmware_version": "20230913-112003",
"available_firmware_version": "1.2.0",
"response_time": 0.123,
"last_seen": "2024-01-15T10:30:00Z"
}
]curl -X POST http://localhost:8000/api/devices/192.168.1.100/update \
-H "Content-Type: application/json" \
-d '{"channel": "stable"}'Response:
{
"ip": "192.168.1.100",
"success": true,
"message": "Update executed successfully on shelly",
"action_type": "shelly.Update",
"channel": "stable",
"source": "internet"
}For a device with no internet access, ask the manager to serve the firmware.
It downloads the official bundle once, keeps it, and hands the device a URL on
this host. Requires SHELLY_FIRMWARE_ADVERTISED_BASE_URL; stable channel only.
curl -X POST http://localhost:8000/api/devices/192.168.1.100/update \
-H "Content-Type: application/json" \
-d '{"source": "local"}'curl "http://localhost:8000/api/devices/192.168.1.100/components/actions"Response Example:
{
"device_ip": "192.168.1.100",
"components": [
{
"component_id": "switch:0",
"component_type": "switch",
"available_actions": [
{
"action": "toggle",
"description": "Toggle switch state",
"parameters": {}
},
{
"action": "turn_on",
"description": "Turn switch on",
"parameters": {}
}
]
}
]
}curl -X POST "http://localhost:8000/api/devices/192.168.1.100/components/switch:0/action" \
-H "Content-Type: application/json" \
-d '{"action": "toggle", "params": {}}'Request Body:
{
"action": "toggle",
"params": {}
}Response:
{
"ip": "192.168.1.100",
"component_id": "switch:0",
"action": "toggle",
"success": true,
"result": {
"new_state": "on"
}
}curl -X POST "http://localhost:8000/api/devices/192.168.1.100/components/cover:0/action" \
-H "Content-Type: application/json" \
-d '{"action": "open", "params": {}}'curl -X POST "http://localhost:8000/api/credentials" \
-H "Content-Type: application/json" \
-d '{"mac": "AABBCCDDEEFF", "password": "mypassword", "username": "admin"}'Response:
{
"mac": "AABBCCDDEEFF",
"username": "admin",
"last_seen_ip": null
}curl "http://localhost:8000/api/credentials"Response:
[
{
"mac": "AABBCCDDEEFF",
"username": "admin",
"last_seen_ip": "192.168.1.100"
}
]curl -X DELETE "http://localhost:8000/api/credentials/AABBCCDDEEFF"curl -X POST "http://localhost:8000/api/backups" \
-H "Content-Type: application/json" \
-d '{"device_ip": "192.168.1.100", "name": "before-upgrade"}'Response:
{
"id": 1,
"device_mac": "AABBCCDDEEFF",
"device_ip": "192.168.1.100",
"device_name": "Living Room Light",
"generation": "gen2",
"name": "before-upgrade",
"source": "manual",
"size_bytes": 2048,
"created_at": 1718800000
}curl -X POST "http://localhost:8000/api/backups/1/restore" \
-H "Content-Type: application/json" \
-d '{"device_ip": "192.168.1.100", "component_keys": ["switch:0", "sys"]}'Response:
{
"success": true,
"device_ip": "192.168.1.100",
"backup_id": 1,
"total": 2,
"succeeded": 2,
"failed": 0,
"skipped": 0,
"components": [
{ "key": "switch:0", "action": "SetConfig", "success": true },
{ "key": "sys", "action": "SetConfig", "success": true }
]
}{
"detail": "Device not reachable",
"status_code": 404,
"ip": "192.168.1.100"
}| Variable | Default | Description |
|---|---|---|
HOST |
0.0.0.0 |
API server host, all interfaces by default |
PORT |
8000 |
API server port |
DEBUG |
false |
Enable debug mode |
SHELLY_SECRET_KEY |
(required) | Fernet key for credential encryption. Generate with: openssl rand -base64 32 | tr '+/' '-_' |
SHELLY_BACKUP_SCHEDULER_ENABLED |
true |
Run the in-process scheduled-backup poller |
SHELLY_BACKUP_POLL_INTERVAL_SECONDS |
60 |
How often the scheduler checks for due backups |
SHELLY_FIRMWARE_ADVERTISED_BASE_URL |
(none) | URL devices use to reach this API, e.g. http://192.168.1.50:8000. Required for local updates; it cannot be guessed |
SHELLY_DATA_DIR |
/data in the image, ./data otherwise |
Directory holding the SQLite database (data.db) |
SHELLY_FIRMWARE_DIR |
/data/firmware in the image, ./data/firmware otherwise |
Where downloaded firmware bundles are kept |
SHELLY_FIRMWARE_INDEX_URL |
https://updates.shelly.cloud/update |
Where published firmware is looked up, by the app name a device reports |
SHELLY_FIRMWARE_ALLOWED_DOWNLOAD_HOSTS |
shelly.cloud |
Comma separated hosts firmware may be downloaded from, matched exactly or as a parent domain and re-checked on every redirect. * accepts any host |
SHELLY_FIRMWARE_VERIFY_SSL |
false |
Verify TLS when talking to the firmware index and CDN. Off by default because Shelly signs those hosts with a private CA absent from public trust stores; devices verify firmware signatures themselves |
SHELLY_SECRET_KEY also encrypts configuration backup snapshots at rest. Backups are
stored in the local database ({data_dir}/data.db); rotating the key makes existing snapshots
undecryptable. Set SHELLY_BACKUP_SCHEDULER_ENABLED=false to turn off automated backups while
still managing schedules through the API.
Both directories are created on first use and both need to survive a restart, so mount a
volume at /data when running this image. Without one they belong to the container, and
replacing it resets the database and empties the firmware cache. A named volume inherits the
image's ownership; a bind mounted host directory does not, so chown 10001:10001 it before
first start or the API cannot write to it. Packaged builds set their own path: the Unraid
image uses /config, the Home Assistant add-on uses the add-on data volume, and neither
needs a mount configured by hand.
docker run -d \
--name shelly-manager-api \
-p 8000:8000 \
-e HOST=0.0.0.0 \
-e PORT=8000 \
-e SHELLY_SECRET_KEY="your-generated-key" \
-v shelly-manager-data:/data \
ghcr.io/jfmlima/shelly-manager-api:latestservices:
api:
image: ghcr.io/jfmlima/shelly-manager-api:latest
ports:
- "8000:8000"
environment:
- HOST=0.0.0.0
- PORT=8000
- DEBUG=false
- SHELLY_SECRET_KEY=your-generated-key
volumes:
- shelly-manager-data:/data
restart: unless-stopped
volumes:
shelly-manager-data:healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:8000/api/health || exit 1"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s# From project root
cd shelly-manager
# Install with development dependencies
uv sync --package shelly-manager-api --extra dev
# Run with auto-reload
uv run --package shelly-manager-api python -m api.main
# Run tests
uv run --package shelly-manager-api pytest packages/api/tests/ -v
# Run linting
uv run ruff check packages/api/
uv run mypy packages/api/src/apiThe API uses Litestar's built-in OpenAPI generation:
- Interactive Docs: http://localhost:8000/docs (Swagger UI)
- OpenAPI Spec: http://localhost:8000/schema/openapi.json
- Create Controller: Add to
packages/api/src/api/controllers/ - Define DTOs: Add request/response models to
packages/api/src/api/presentation/dto/ - Add Route: Register controller in
packages/api/src/api/main.py - Add Tests: Create tests in
packages/api/tests/
Example controller:
from litestar import Controller, get
from api.presentation.dto.responses import DeviceResponse
class DeviceController(Controller):
path = "/devices"
@get("/{device_ip:str}/status")
async def get_device_status(self, device_ip: str) -> DeviceResponse:
# Implementation here
pass# Run all API tests
make test-api
# Run specific test files
uv run --package shelly-manager-api pytest packages/api/tests/unit/controllers/ -v
# Run with coverage
uv run --package shelly-manager-api pytest packages/api/tests/ --cov=api --cov-report=htmlThe API follows Clean Architecture principles:
packages/api/src/api/
├── controllers/ # HTTP request handlers
├── dependencies/ # Dependency injection container
├── main.py # Application entry point
└── presentation/
├── dto/ # Data Transfer Objects
└── serializers/ # Response serialization
- Litestar: Modern async web framework
- Pydantic: Data validation and serialization
- uvicorn: ASGI server
- Core Package: Business logic and domain models
- Port already in use: Change
PORTenvironment variable - CORS errors: Configure CORS settings for your web UI domain
- Health check failures: Verify API is responding on configured port
Enable debug mode for detailed error messages:
docker run -p 8000:8000 \
-e DEBUG=true \
ghcr.io/jfmlima/shelly-manager-api:latest# View container logs
docker logs shelly-manager-api
# Follow logs in real-time
docker logs -f shelly-manager-api- Main Documentation: ../../README.md
- Development Guide: ../../DEVELOPMENT.md
- Core Package: ../core/README.md
- CLI Package: ../cli/README.md