Publish LumaOps source
This commit is contained in:
@@ -0,0 +1,256 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user