The core layer of the OpenLamp family: instant, 100% local control of smart LED lamps (WLED recommended, Tuya also supported), exposed through a stable command contract — OpenLamp State (OLS), a WLED-compatible JSON state patch (see OLS.md).
Part of the OpenLamp family:
| Layer | Repo | Role |
|---|---|---|
| Engine (this repo) | openlamp/engine |
drivers + dispatcher + local API + daemon + CLI (Python reference) |
| Engine, JS port | engine-js | same contract on Node/tuyapi — for JS-first environments |
| Ableton Live frontend | live | drive lamps from a Live set (emits the wled-midi convention) |
| MIDI convention | wled-midi | the MIDI↔WLED spec this engine implements (see midi.py) |
| Ableton Link / tempo | openlamp-midi | beat / tempo follow (beatsync) |
The engine is the reference implementation of the
wled-midi convention. Run
midi.py to open a virtual MIDI input port (default OpenLamp) and drive
the lamps from any DAW or controller — notes → colours, CC → brightness/effects,
Program Change → presets, MIDI clock → tempo:
python3 midi.py # opens the "OpenLamp" virtual MIDI portAny frontend that speaks the convention works: Ableton,
the Stream Deck MIDI plugin (se.trevligaspel.midi, for scripted sequences), or a
hardware pad/fader controller. (This replaces the old openlamp-midi
bridge — the MIDI control path now lives here; openlamp-midi keeps the Ableton Link
tempo-follow.)
The one-shot command-line frontend is published as openlamp-lamp on PyPI:
pip install openlamp-lamp
lamp rouge # set colour (keeps brightness)
lamp bri:60 # brightness 1-100
lamp veilleuse # a brightness preset (lueur/veilleuse/tamise/moyen/fort/max)
lamp on | offIt finds your lamp config (with local keys) from $OPENLAMP_LAMPS, else
~/.config/openlamp/tuya-lamps.json (template at the bottom of this README). If the
OpenLamp daemon is running, the CLI routes through its local API
(127.0.0.1:8377) for instant response; otherwise it drives the lamps directly.
Only the CLI (
lamp.py) ships on PyPI. The engine + headless daemon stay in this repo (they couple to a local file layout — a clean engine package is a follow-up).
engine.py— the engine: one thread per lamp with a persistent connection (sub-200 ms commands), the OLS dispatcher, groups, snapshots, animations (cycle/flash/tempo), connect-time sync, a rainbow welcome sweep, and the local API on127.0.0.1:8377(/cmd,/status,/syntax, plus the optional WLED-compat/json/state). Frontend-agnostic: its only upward link is anon_changehook.daemon.py— headless host: runs the engine without any frontend app.run-headless.sh— one command to switch to CLI/MIDI-only mode.lamp-doctor.sh(macOS) — one-command diagnosis of "lamps unreachable", testing the three causes in order: Mac on the wrong Wi-Fi / router down / lamp powered off or radio-napping. Never trust the router's web panel (it renders from service-worker cache even with the router dead) — this script is the ground truth.lamp.py— the CLI (also Bome-callable):lamp.py vert,lamp.py bri:40… Routes through the local API when a host runs, drives lamps directly otherwise.bin/lamp-bench.py— a small bench for WLED tinkerers: firmware/hardware info, command latency (the round-trip a press pays), and — opt-in — the command ceiling (--ceiling) the lamp sustains before dropping, plus a conformance check (--check, write commands and read them back). stdlib only.lamp-bench.py <ip>.com.openlamp.daemon.plist— launchd autostart for the daemon.app.py+packaging/— the headless desktop app: one process running the engine- the wled-midi bridge, with an optional tray, bundled by PyInstaller into a
.app/.exe. See Desktop app.
- the wled-midi bridge, with an optional tray, bundled by PyInstaller into a
OLS.md— the OpenLamp State contract.TUYA-KEYS.md— how to get your lamps' local keys (official Tuya cloud API, one-time).
app.py runs the engine + the wled-midi MIDI bridge in one process (tray optional). Build a
distributable bundle:
packaging/build.sh # -> dist/OpenLamp.app (macOS) / dist/OpenLamp/ (Windows)The build is free and the unsigned bundle runs for personal use (right-click → Open, or
xattr -d com.apple.quarantine OpenLamp.app).
⏸ Code-signing + notarising is deferred — a later task, done only when we publish a public download. It exists only to remove the Gatekeeper/SmartScreen warning for third-party downloaders, and its one cost (Apple Developer Program, 99 $/yr) isn't worth paying until there's a public release. The full how-to is ready in
packaging/SIGNING.mdfor that day. Decision recorded 2026-07-16.
The engine carries no Ableton Link / aalink code and never links it. Beat-sync from
an Ableton Link session is handled by a separate
process — openlamp-midi (beatsync.py) — which
reaches the engine only over this local HTTP API on 127.0.0.1:8377 (/cmd,
/status).
Because Ableton Link (via aalink) is GPLv2, that
separation matters: the GPL combined-work lives entirely inside the openlamp-midi
process. The engine is a genuinely separate program communicating over HTTP, so it stays
under its own permissive license and the GPL obligation does not propagate to it. Full
write-up in the openlamp-midi README.
Every host binds port 8377 (and a Tuya lamp additionally accepts only one local connection).
So run either the Stream Deck plugin (it embeds this engine in-process) or
daemon.py — never both. Deck sessions → plugin; CLI/MIDI-only sessions → daemon.
OLS uses 8-bit for brightness and per-channel color, for three reasons:
- WLED compatibility — WLED's JSON API is 8-bit; OLS is a compatible patch.
- It matches the hardware — RGB LEDs are driven 8 bits per channel (16.7 M colors); Tuya's internal 0–1000 scale adds no perceptible precision.
- It matches perception — ~1 % brightness steps are at the threshold of what the eye distinguishes; 256 levels cover that. MIDI frontends (7-bit, 0–127) scale up ×2 — plenty for stage cues.
You can point the engine at an external, synced folder (Google Drive/Dropbox) that
holds lamp.py + tuya-lamps.json by setting OPENLAMP_LAMPS_DIR to that path;
otherwise tuya-lamps.json sits next to lamp.py (never committed — it contains your local
keys). Template:
{
"lamps": [
{"name": "L1", "mac": "aa:bb:cc:dd:ee:ff", "device_id": "…", "local_key": "…",
"ips": {"192.168.1": "192.168.1.50"}}
],
"groups": {"front": ["L1"]},
"sync": {"enabled": true, "state": {"on": true, "col": [0, 100, 200], "bri": 153}}
}The engine is developed and tested on this cheap, off-the-shelf rig — reproduce it for well under €40 for a two-lamp stereo stage:
- Bulbs — Athom WLED 7 W Color Bulb (E27, ESP32-C3, RGB + tunable white, WLED-preflashed, ~€13 each). WLED ships already flashed — put the bulb on Wi-Fi and it's auto-discovered. This is the reference bulb every timing figure is measured on (~45 ms/command, the latency-comp floor beatsync uses).
- Socket / holder — TobeBright E27 corded lamp holder with inline switch (up to 100 W, a few € on Amazon). Turns a bare bulb into a standalone plug-in stage lamp — no fixture required, just screw the bulb in and plug it to mains.
openlamp-lamp publishes via Trusted Publishing (OIDC) — no token in the repo.
.github/workflows/publish.yml builds + publishes on each GitHub Release. One-time:
- PyPI → Add a pending publisher (https://pypi.org → Publishing): project
openlamp-lamp, owneropenlamp, repoengine, workflowpublish.yml, environmentpypi. - GitHub → Settings → Environments → New →
pypi. - Bump
versioninpyproject.toml, commit, cut a GitHub Release (tagv0.1.0) → the workflow builds and publishes. Thenpip install openlamp-lampworks everywhere.
Made by @Beennnn (OpenLamp) with the help of Claude. WLED is the recommended, tested path (validated on Athom RGBCW bulbs, ~45 ms/command). Feedback: open an issue on engine.