257 lines
11 KiB
Markdown
257 lines
11 KiB
Markdown
# LumaOps architecture
|
|
|
|
## System shape
|
|
|
|
LumaOps is a single-container, local-first control plane around the imported
|
|
OpenRGB 1.0rc3 hardware engine.
|
|
|
|
```text
|
|
Browser
|
|
│ HTTP / SSE on WEB_PORT (bridge default 1223)
|
|
▼
|
|
FastAPI application ───── SQLite + encrypted connector secrets
|
|
│
|
|
├── command service ─── audit, idempotency, rate limits, locks
|
|
│ │
|
|
│ └── connector registry
|
|
│ ├── OpenRGB connector ── SDK v5 TCP ── OpenRGB Core
|
|
│ ├── WLED connector HTTP/UDP (optional)
|
|
│ ├── Home Assistant REST/WS (optional)
|
|
│ └── future connectors
|
|
│
|
|
└── built React application
|
|
|
|
Container supervisor
|
|
├── OpenRGB: 127.0.0.1:6742 only
|
|
└── FastAPI: APP_HOST:APP_PORT
|
|
```
|
|
|
|
Only the internal `APP_PORT` is mapped to `WEB_PORT`; the defaults are
|
|
`1223:8080`. The OpenRGB SDK has no authentication or encryption and is never a
|
|
public container port.
|
|
|
|
## Repository boundaries
|
|
|
|
- The imported OpenRGB tree remains at the repository root.
|
|
- `lumaops/backend` contains the FastAPI package and Python tests.
|
|
- `lumaops/frontend` contains the React/TypeScript application.
|
|
- `lumaops/backend/src/lumaops_backend/migrations` contains ordered SQLite migrations.
|
|
- `docker` contains entrypoint/supervision, health, and Unraid assets.
|
|
- `docs` contains LumaOps operational and design documentation.
|
|
- A minimal OpenRGB core patch is isolated and documented separately.
|
|
|
|
## Runtime lifecycle
|
|
|
|
1. Tini becomes PID 1 and forwards container signals.
|
|
2. The LumaOps supervisor creates persistent directories and validates their
|
|
permissions.
|
|
3. It starts OpenRGB with an explicit loopback address and persistent config
|
|
path, then captures OpenRGB output in `/logs/openrgb.log`.
|
|
4. It starts FastAPI regardless of whether hardware detection succeeds.
|
|
5. FastAPI runs migrations, creates an application instance ID/keyfile if
|
|
needed, and starts the connector registry.
|
|
6. The OpenRGB connector negotiates SDK protocol 5 and synchronizes inventory.
|
|
7. A device-list event or reconnect schedules another read-only inventory sync.
|
|
8. On SIGTERM/SIGINT, FastAPI stops accepting work, connector tasks are
|
|
cancelled, OpenRGB receives SIGTERM, and the supervisor waits before a
|
|
bounded SIGKILL fallback.
|
|
|
|
An OpenRGB failure degrades hardware functions but does not take down the web
|
|
application. The supervisor restarts OpenRGB with bounded backoff. Repeated
|
|
failure opens a circuit and surfaces a recovery action in Diagnostics.
|
|
|
|
## Backend modules
|
|
|
|
The application follows ports-and-adapters boundaries rather than exposing SDK
|
|
types through the API:
|
|
|
|
- `connectors.base`: normalized capabilities, inventory, state, health, errors,
|
|
and connector contract.
|
|
- `connectors.openrgb.protocol`: bounded SDK-v5 parser and serializer.
|
|
- `connectors.openrgb.adapter`: connection, handshake, request correlation,
|
|
reconnect, profile operations, and change events.
|
|
- `connectors.mock`: deterministic test/development implementation that can be
|
|
enabled only by an explicit non-production setting.
|
|
- `services.inventory`: stable identities and reconciliation.
|
|
- `services.commands`: validation, emergency stop, idempotency, locks, rate
|
|
limiting, execution records, and audit.
|
|
- `services.scenes`: capture/apply/preview and compensating rollback.
|
|
- `services.automations`: schedule evaluation, cooldown, conflict keys, and run
|
|
history.
|
|
- `services.setup`: read-only environment and hardware checks.
|
|
- `services.diagnostics`: redacted support bundle generation.
|
|
- `api`: versioned resource routers under `/api/v1`, plus SSE.
|
|
|
|
## Connector contract
|
|
|
|
Every connector exposes:
|
|
|
|
- lifecycle: `start`, `stop`, `test`, and `health`;
|
|
- discovery and inventory without implicit writes;
|
|
- normalized capabilities and current state;
|
|
- validated `set_state` operations;
|
|
- optional profile/preset operations;
|
|
- timeouts, bounded retries, rate limits, and normalized error categories;
|
|
- a configuration schema whose secret fields are explicitly marked;
|
|
- an inventory-change event callback.
|
|
|
|
The UI operates only on normalized devices and capability flags. It never
|
|
contains an OpenRGB packet, WLED segment, or Home Assistant service-call path.
|
|
|
|
## Device identity and ownership
|
|
|
|
An inventory record has an immutable internal UUID. A connector provides a
|
|
candidate fingerprint from the most durable available fields, in priority
|
|
order: connector ID, hardware serial, stable location/path, MAC/IP plus model,
|
|
then a guarded composite of vendor/model/name. The `device_identities` table
|
|
persists all observed aliases so a controller moving to another discovery index
|
|
keeps its UUID.
|
|
|
|
`controller_index` is volatile routing data and is updated during each OpenRGB
|
|
sync. Before every write, the adapter compares the stored fingerprint with the
|
|
current controller at that index. Mismatch cancels the command and triggers a
|
|
resync.
|
|
|
|
One physical light has one management owner: OpenRGB, native connector, Home
|
|
Assistant, or unmanaged. Possible duplicates are surfaced for administrator
|
|
resolution and are never written through two connectors concurrently.
|
|
|
|
## Normalized capability model
|
|
|
|
Capabilities are explicit booleans plus ranges:
|
|
|
|
- power and restore;
|
|
- RGB, colour temperature, and brightness;
|
|
- effect/mode, speed, direction, and multiple colours;
|
|
- per-zone, per-segment, and per-LED control;
|
|
- presets/profiles;
|
|
- read-only or experimental state.
|
|
|
|
Commands are intersected with capabilities. Unsupported properties result in a
|
|
422 response before any connector is called. Brightness, speed, indices,
|
|
colours, list lengths, and update frequency are range-checked twice: at the API
|
|
boundary and in the connector.
|
|
|
|
## Command path and safety
|
|
|
|
Every mutation follows this path:
|
|
|
|
```text
|
|
API validation
|
|
→ authentication/CSRF policy
|
|
→ idempotency lookup
|
|
→ emergency-stop/read-only/block policy
|
|
→ stable identity resolution
|
|
→ capability validation
|
|
→ connector + device locks
|
|
→ token-bucket rate limit
|
|
→ connector timeout/circuit breaker
|
|
→ result + audit record + realtime event
|
|
```
|
|
|
|
Commands have UUIDs and record request ID, actor, target, sanitized desired
|
|
state, start/end time, duration, result, and normalized error. Secret values and
|
|
raw environment data are never command payloads.
|
|
|
|
The emergency stop cancels queued effects, prevents new hardware writes, and
|
|
sends one bounded all-off attempt only when the administrator requests it. It
|
|
does not loop on failing hardware.
|
|
|
|
## Scenes and rollback
|
|
|
|
LumaOps scenes are application-level, versioned desired-state documents. They
|
|
can contain OpenRGB devices, connector devices, groups, and explicit off
|
|
entries. OpenRGB profiles remain separate objects.
|
|
|
|
Scene application first resolves targets and snapshots retrievable prior state.
|
|
Commands then execute in deterministic connector/device order. If a required
|
|
item fails, already changed devices receive compensating commands where their
|
|
prior state is known. The result reports applied, skipped, failed, and rolled
|
|
back targets; rollback is best effort and never hides the original failure.
|
|
|
|
## Automation model
|
|
|
|
Rules store a trigger document, action list, timezone, enabled state, cooldown,
|
|
and conflict key. MVP trigger types are time, weekday, manual, webhook, and
|
|
device online/offline. Sunrise/sunset becomes active once coordinates are
|
|
configured. Temperature and Home Assistant events use the same trigger
|
|
interface when a provider exists.
|
|
|
|
A single scheduler claims due rules transactionally. The conflict key prevents
|
|
overlapping runs for the same scene/group/device. All attempts create an
|
|
`automation_runs` record, including skipped cooldown/conflict decisions.
|
|
|
|
## Data and migrations
|
|
|
|
SQLite uses foreign keys, WAL mode, a busy timeout, UTC timestamps, and explicit
|
|
transactions. Ordered SQL migrations are recorded in `schema_migrations`.
|
|
Before a migration marked destructive, the runner creates a timestamped online
|
|
backup in `/data/backups` and aborts if that backup fails.
|
|
|
|
Core tables cover settings, connectors, encrypted secrets, devices, identity
|
|
aliases, rooms, groups, membership/tags, scenes/items, automations/runs,
|
|
commands/results, audit events, health samples, discovery runs, ignored
|
|
devices, idempotency keys, and setup state.
|
|
|
|
Soft deletion is used for user-managed resources and history-bearing inventory.
|
|
High-volume health and activity history has a configurable retention policy.
|
|
|
|
## API and realtime updates
|
|
|
|
REST resources live below `/api/v1`. Responses carry `X-Request-ID`; failures
|
|
use one stable shape:
|
|
|
|
```json
|
|
{
|
|
"error": {
|
|
"code": "device_offline",
|
|
"message": "Het apparaat is niet bereikbaar.",
|
|
"request_id": "...",
|
|
"details": {},
|
|
"recovery": ["Controleer de connector", "Voer een nieuwe scan uit"]
|
|
}
|
|
}
|
|
```
|
|
|
|
List endpoints use `limit`, `offset`, filtering, and documented sort fields.
|
|
Writes that could be retried accept `Idempotency-Key`. Server-Sent Events at
|
|
`/api/v1/events` distribute health, inventory, command, and audit changes.
|
|
|
|
FastAPI also serves the compiled SPA and its hashed assets. API and health paths
|
|
are excluded from SPA fallback.
|
|
|
|
## Configuration and secrets
|
|
|
|
Non-secret settings come from environment variables and the settings table;
|
|
environment values win for deployment-critical addresses and paths. Connector
|
|
secrets are encrypted with Fernet-compatible authenticated encryption using a
|
|
persistent key from `LUMAOPS_SECRET_KEY` or `/config/lumaops/secret.key`.
|
|
|
|
The keyfile is created with mode 0600 when absent. Losing it makes connector
|
|
secrets unrecoverable but does not corrupt inventory, scenes, or audit data.
|
|
Rotation decrypts all values in one transaction using the old key and reencrypts
|
|
them with the new key after a backup.
|
|
|
|
## Health model
|
|
|
|
`/health/live` reports only whether the web process can serve requests.
|
|
`/health/ready` verifies database access and migrations. `/api/v1/health`
|
|
aggregates the supervisor heartbeat, OpenRGB process, SDK protocol/connection,
|
|
database, connectors, and persistent-directory writability.
|
|
|
|
Aggregate states are `healthy`, `degraded`, and `unhealthy`. Missing hardware or
|
|
SDK is degraded, not a web-process crash. The container healthcheck treats a
|
|
working but degraded application as alive while exposing the degradation in the
|
|
detailed API.
|
|
|
|
## Deployment invariants
|
|
|
|
- x86_64 Linux and one production image.
|
|
- No compiler, source tree, npm cache, or build tool in the runtime stage.
|
|
- `/config`, `/data`, and `/logs` are persistent and writable.
|
|
- No default `privileged: true`; only explicit device mappings.
|
|
- Bridge networking by default; documented host mode for LAN discovery.
|
|
- No SaaS dependency and no automatic reverse-proxy or external exposure.
|
|
- Mock mode is rejected when `LUMAOPS_ENV=production` and is visibly marked in
|
|
every page when explicitly enabled elsewhere.
|