Skip to content

Latest commit

 

History

History
631 lines (483 loc) · 17.5 KB

File metadata and controls

631 lines (483 loc) · 17.5 KB

Shelly Manager API

REST API server for Shelly Manager built with Litestar.

Features

  • 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

Quick Start

Docker

# Run API server
docker run -p 8000:8000 \
  ghcr.io/jfmlima/shelly-manager-api:latest

# Visit API documentation
open http://localhost:8000/docs

Local Development

# 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/docs

API Endpoints

Health & Status

GET /api/health                    # Service health check

Device Discovery

GET /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 workers

Device Operations

GET /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 action

Firmware Store

Bundles 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 fetches

Configuration Backup & Restore

Per-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/apply does 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 backup

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

Scheduled Backups

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 time

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

Component Actions

The Component Actions system provides dynamic action discovery and execution for individual device components.

Discovery

GET /api/devices/{ip}/components/actions    # Get all available actions for device

Execute Component Action

POST /api/devices/{ip}/components/{component_id}/action

Credentials Management

For 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 credentials

Monitoring

GET /api/health

Examples

Device Scan

curl "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"
  }
]

Device Update

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"}'

Component Actions Examples

Discover Device Actions

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": {}
        }
      ]
    }
  ]
}

Toggle a Switch

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"
  }
}

Open a Cover

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": {}}'

Credentials Management Examples

Set Device Credentials

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
}

List Stored Credentials

curl "http://localhost:8000/api/credentials"

Response:

[
  {
    "mac": "AABBCCDDEEFF",
    "username": "admin",
    "last_seen_ip": "192.168.1.100"
  }
]

Delete Credentials

curl -X DELETE "http://localhost:8000/api/credentials/AABBCCDDEEFF"

Backup & Restore Examples

Capture a Backup

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
}

Restore a Backup

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 }
  ]
}

Error Response

{
  "detail": "Device not reachable",
  "status_code": 404,
  "ip": "192.168.1.100"
}

Configuration

Environment Variables

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 Deployment

Basic Deployment

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:latest

Docker Compose

services:
  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:

Health Check

healthcheck:
  test: ["CMD-SHELL", "curl -f http://localhost:8000/api/health || exit 1"]
  interval: 30s
  timeout: 10s
  retries: 3
  start_period: 40s

Development

Local Development Setup

# 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/api

API Documentation Development

The API uses Litestar's built-in OpenAPI generation:

Adding New Endpoints

  1. Create Controller: Add to packages/api/src/api/controllers/
  2. Define DTOs: Add request/response models to packages/api/src/api/presentation/dto/
  3. Add Route: Register controller in packages/api/src/api/main.py
  4. 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

Testing

# 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=html

Architecture

The 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

Dependencies

  • Litestar: Modern async web framework
  • Pydantic: Data validation and serialization
  • uvicorn: ASGI server
  • Core Package: Business logic and domain models

Troubleshooting

Common Issues

  1. Port already in use: Change PORT environment variable
  2. CORS errors: Configure CORS settings for your web UI domain
  3. Health check failures: Verify API is responding on configured port

Debug Mode

Enable debug mode for detailed error messages:

docker run -p 8000:8000 \
  -e DEBUG=true \
  ghcr.io/jfmlima/shelly-manager-api:latest

Logs

# View container logs
docker logs shelly-manager-api

# Follow logs in real-time
docker logs -f shelly-manager-api

Additional Resources