Files

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.