An OpenDeck plugin for controlling Elgato Key Light devices, directly over the network. Includes full Stream Deck+ dial support.
Unofficial community plugin. Not affiliated with or endorsed by Elgato or Corsair. "Elgato" and "Key Light" are trademarks of Corsair Memory, Inc.
The plugin talks to the lights directly over their local HTTP API (port 9123) and finds
them on the network with mDNS (_elg._tcp). Nothing else needs to be running.
┌─────────────┐ WebSocket ┌─────────────────────┐ HTTP :9123 ┌────────────┐
│ OpenDeck │ ◄────────► │ opendeck-keylight │ ─────────────► │ Key Lights │
│ (SD device) │ (SDK proto)│ (this plugin, Node) │ ◄─ mDNS ────── │ │
└─────────────┘ └─────────────────────┘ └────────────┘
Versions before 0.4.0 went through keylight-control's HTTP API instead; that's no longer needed.
| Platform | Status |
|---|---|
| Linux | Tested (OpenDeck 2.14, Ubuntu) |
| Windows | Untested. Should work: the plugin is plain Node.js with no native code. Discovery shares mDNS port 5353 with the system's own responder, which hasn't been tried on Windows; if lights don't show up, use Manual IPs. |
| macOS | Untested. Same as Windows: expected to work, with Manual IPs as the fallback if discovery comes up empty. |
Reports from Windows and macOS users are welcome in the issue tracker.
- OpenDeck installed
- Node.js 20+ on the system
PATH(OpenDeck runs plugins with the systemnode) - The lights reachable from this machine on TCP 9123. For automatic discovery, mDNS (UDP 5353) must reach them too: same network, or an mDNS reflector across VLANs. Otherwise, enter their IPs under Manual IPs in any Key Light action's settings.
From OpenDeck's plugin store: open the plugin manager, find "Key Light Direct" in the Plugin store and install it.
From a release: download me.dwot.keylight-X.Y.Z.streamDeckPlugin from the
latest release, then in
OpenDeck's plugin manager click Install from file. Releases bundle their dependencies.
From source:
git clone https://github.com/dwot/opendeck-keylight.git
cd opendeck-keylight
./install.shThen restart OpenDeck.
The script auto-detects native (~/.config/opendeck/plugins) vs. Flatpak OpenDeck
installs, copies the plugin there, and runs npm install only if ws isn't already
available (Debian/Ubuntu: sudo apt install node-ws).
| Action | Rotate | Press / Touch |
|---|---|---|
| Brightness Dial | Adjust brightness (configurable step %) | Toggle on/off |
| Temperature Dial | Adjust color temperature, 2900–7000K | Reset to configured default (e.g. 4500K) |
Both dials show live feedback on the touchscreen LCD strip — light name, current value, and a progress bar.
| Action | Behaviour |
|---|---|
| Toggle Light | Toggle a single light or all lights at once |
| Set Brightness | Set a fixed brightness on key press (good for scene presets like 25% / 100%) |
| Apply Scene | Combined power + brightness + temperature preset |
All actions support targeting an individual light or All Lights. When using "All Lights" with a dial, both lights track together using the first discovered light as the reference value.
Every Key Light action's settings panel has a shared footer:
- Rescan: run discovery again (also runs every 5 minutes, and whenever a light stops answering).
- Manual IPs: comma-separated
iporip:portlist, probed alongside mDNS results. Only needed when mDNS can't reach the lights.
Lights are identified by MAC address, so a light keeps its key bindings if DHCP gives it a new IP. The last-known lights are cached in OpenDeck's global plugin settings, so keys work immediately on startup, before discovery finishes. Light names come from the name you set in Elgato Control Center.
.
├── manifest.json Stream Deck SDK plugin manifest (Elgato-valid: mac/windows)
├── manifest.linux.json OpenDeck-only override adding the linux platform
├── package.json Node deps (just `ws`)
├── bin/
│ ├── plugin.js Main plugin: WebSocket to OpenDeck, action handlers
│ ├── keylights.js Elgato HTTP client + device registry (discovery, cache, polling)
│ └── mdns.js Dependency-free mDNS browser for _elg._tcp
├── propertyInspector/ HTML config UIs shown in OpenDeck for each action
│ ├── common.js Shared SDK boilerplate
│ ├── style.css
│ ├── toggle.html
│ ├── setbrightness.html
│ ├── scene.html
│ ├── brightnessdial.html
│ └── temperaturedial.html
├── icons/ PNG icons (key images, action list, category, plugin)
├── install.sh Install to OpenDeck plugins dir
├── build.sh Produce a distributable .streamDeckPlugin zip
├── CHANGELOG.md
└── .github/workflows/
└── release.yml Tag-driven build + validate + GitHub release
Bump Version in manifest.json (4-part, X.Y.Z.0) and version in package.json (X.Y.Z), add a CHANGELOG.md
section, then tag and push vX.Y.Z. .github/workflows/release.yml checks that the tag
matches the manifest, builds, runs streamdeck validate and publishes the
.streamDeckPlugin as the release's only asset (on the GitHub mirror).
To produce a .streamDeckPlugin file (zip) for distribution:
./build.sh
# Output: dist/me.dwot.keylight-X.Y.Z.streamDeckPluginOpenDeck's GUI plugin installer accepts .streamDeckPlugin files. The repo is also
directly installable via install.sh without building.
- State polling: every light's
GET /elgato/lightsis polled every 5s to keep button visuals synced with external changes (phone app, Control Center). Writes usePUT /elgato/lights, and the reply (the light's new state) updates the keys immediately. - Discovery:
bin/mdns.jsjoins 224.0.0.251:5353 withSO_REUSEADDR(coexists with avahi), sends a PTR query for_elg._tcp.localand resolves SRV/A/TXT records. Each hit is confirmed withGET /elgato/accessory-info. A light that fails 3 polls in a row is dropped until the next successful discovery. - Legacy settings: keys configured under 0.3.x stored keylight-control's numeric index; those resolve to the Nth light sorted by name until the key is re-saved.
- Dial debouncing: dial rotates apply optimistic local UI immediately, then debounce HTTP calls by 80ms — spinning the dial fires 1–2 requests, not 50.
- Temperature units: the API speaks Elgato's native units (143–344). The plugin converts to/from Kelvin for the UI. 143 ≈ 7000K, 344 ≈ 2900K, linear interp.
- All Lights toggle: smart group toggle. If any light is on, all get turned off; if all are off, all get turned on. Avoids out-of-sync states.
OpenDeck plugin logs:
~/.local/share/opendeck/logs/
The plugin writes to stdout/stderr, captured by OpenDeck.
MIT — see LICENSE.