| .gitignore | ||
| artnet.go | ||
| config.example.yaml | ||
| config.go | ||
| config_test.go | ||
| device.go | ||
| device_test.go | ||
| discover.go | ||
| flags.go | ||
| go.mod | ||
| go.sum | ||
| LICENSE | ||
| lutron-control.service | ||
| main.go | ||
| mqtt.go | ||
| osc.go | ||
| osc_test.go | ||
| README.md | ||
| sacn.go | ||
| source.go | ||
| transport.go | ||
lutron-control
Control a Lutron GRAFIK Eye QS lighting unit from two independent worlds at once:
- A lighting board / theatrical software — run the GRAFIK Eye's zones as part of a larger light show.
- Home Assistant — control and automate the lights like any other smart light in your house.
It runs as a small always-on program on a computer wired (or networked) to the GRAFIK Eye: it listens for commands and translates them into the GRAFIK Eye's own integration language. A Raspberry Pi is the cheap, common choice this guide walks through, but any Linux/macOS/Windows machine works. Every control method is independent and optional — enable only the ones you need.
New to the jargon? Here's the short version:
- GRAFIK Eye QS — the Lutron lighting control unit this drives.
- QSE-CI-NWK-E — the add-on module on the GRAFIK Eye that gives it a serial port (and a network/telnet interface) we can talk to.
- DMX / sACN (E1.31) / Art-Net — the standard "languages" lighting boards and stage software use. sACN and Art-Net are just DMX sent over your normal network instead of a cable. Only needed if you want lighting-board control.
- MQTT — the messaging system Home Assistant uses to talk to devices. Only needed if you want Home Assistant control.
- OSC (Open Sound Control) — a simple UDP message format used by show controllers and touch-panel apps (e.g. TouchOSC). Only needed if you want to drive the unit from one of those.
Contents
- How it works — the concepts behind devices, sources, and control
- What you'll need — hardware and prerequisites
- Installation — build, install, configure, and run
- Home Assistant & MQTT (Docker)
- Recommended: hardware watchdog
How it works
What it does
You define one or more Lutron devices (each reached over serial or telnet), then attach sources that drive a device's zones:
- sACN (E1.31) — receive network DMX from a lighting console or software.
- Art-Net — receive Art-Net DMX.
- MQTT — expose a device as one or more dimmable lights, each driving its own set of zones, with optional Home Assistant discovery.
- OSC (Open Sound Control) — expose the full control surface over UDP, driven by the message address (so a show controller or a TouchOSC layout can run zones, scenes, shades, and more).
Serial talks to the QSE-CI-NWK-E directly; telnet performs the integration login automatically (TCP keepalive guards against idle-connection drops).
Zones and scenes
DMX sources map a universe's channels to a device's zones (live level control)
and/or scenes (the GRAFIK Eye's 16 presets). Zones are dimmable — the channel
value sets the level. Scenes are not levels: a single scene-select channel
triggers a scene by value (0 = no action, 1 = scene 1, … 16 = scene 16), and
it fires when the value changes to a scene number — so it behaves as a momentary
trigger, not a fader. Set it to a scene number deliberately (from a cue or a button);
sweeping it through a range will fire each scene it passes.
A source only drives the zones it maps — zones left out of the map are untouched, so
they can be owned by another source (e.g. an MQTT light). Use the sequential layout —
zones laid out from start_address, then one scene-select channel — or an explicit
channels map for full control:
sacn:
universe: 3
start_address: 0
zones: 6 # channels 0..5 -> zones 1..6
scenes: 4 # channel 6 -> scene select (value 1..4 triggers a scene, 0 = none)
# channels: # or map explicitly (channel = 1-indexed DMX address)
# - { channel: 1, type: zone, zone: 1 }
# - { channel: 7, type: scene } # value selects the scene to trigger
Over MQTT, define mqtt.lights to expose one or more Home Assistant lights, each
with its own base topic and the zones it controls. A light's brightness drives
every zone in its set, and the first zone is mirrored back as the aggregate state;
all of a source's lights are grouped under one Home Assistant device. Omit the
block to fall back to a single light over every zone using the base topic.
The bridge publishes its liveness to <topic>/availability (online/offline),
registered as the MQTT Last Will so the broker flips it to offline if the bridge
drops, and the discovery configs reference it — so Home Assistant shows the lights
and scene selector as unavailable whenever the bridge is down rather than leaving
stale state behind.
mqtt:
topic: lutron/qse-nwk
lights:
- name: All Zones
topic: lutron/qse-nwk/all
zones: [1, 2, 3, 4, 5, 6] # one light controlling zones 1-6
Set mqtt.scenes: N to expose a scene selector (a Home Assistant select entity
with discovery). A scene that is activated holds until a zone source takes over;
while idle, zone targets follow the panel's reported levels so the bridge doesn't
fight scenes or wall keypads.
The protocol's inter-message delays (100 ms after commands, 1500 ms after queries)
are honored, and #MONITORING is configured on connect so zone/scene feedback
works reliably (even over RS-232).
Arbitration
When several sources target the same device, priority decides who wins. A
higher-priority source that is actively sending locks out lower-priority sources
for hold_sec seconds — so giving DMX sources a higher priority than MQTT lets a
live show keep Home Assistant from changing the lights mid-cue. When an MQTT command
is locked out, the current zone state is mirrored back to MQTT instead.
OSC control
An OSC source exposes the full control surface over UDP. Set osc.listen to the
host:port the server binds and osc.prefix to the address namespace it responds
under; the message address (under that prefix) selects the operation, so the same
source can drive zones, scenes, shades, locks, and sequences. This is the natural
fit for a show controller or a TouchOSC layout. Movement and trigger addresses
(raise/lower/stop, scene-off) act on receipt and ignore their arguments.
sources:
- name: show-osc
type: osc
device: grafik-eye
priority: 5 # Above MQTT, below a live DMX stream.
hold_sec: 5
osc:
listen: 0.0.0.0:9000 # host:port the OSC server binds.
prefix: /lutron # Address namespace this source responds under.
The control addresses, all under prefix:
| Address | Arg | Action |
|---|---|---|
<prefix>/zone/<n>/level |
f or i |
set zone <n> level (float 0–1, or int 0–255) |
<prefix>/zone/<n>/raise |
— | start raising zone <n> |
<prefix>/zone/<n>/lower |
— | start lowering zone <n> |
<prefix>/zone/<n>/stop |
— | stop raising/lowering zone <n> |
<prefix>/scene |
i |
activate scene <i> |
<prefix>/scene/off |
— | activate the scene-off look |
<prefix>/shade/<c>/<act> |
— | drive shade column <c> (open/close/preset/raise/lower/stop) |
<prefix>/lock/zone |
i |
zone lock (0 off, 1 on) |
<prefix>/lock/scene |
i |
scene lock (0 off, 1 on) |
<prefix>/sequence |
i |
sequence (0 off, 1 scenes 1–4, 2 scenes 5–16) |
To stream the panel's feedback back to a controller, set osc.stream_to to one or
more host:port destinations — see Monitoring feedback
below, which covers the symmetric report addresses and the osc.level_as_float
encoding.
Monitoring feedback
Beyond the Home Assistant light/scene entities, the MQTT and OSC sources can relay
the panel's raw monitoring — anything the GRAFIK Eye reports: zone levels, scenes,
keypad button presses, LED states, occupancy, and more. A source's monitor.enable
lists the #MONITORING type numbers to request from the panel (e.g. 3 button,
5 zone, 8 scene; 255 for all); the bridge enables the union across sources on
connect and re-asserts it after a reconnect. monitor.families optionally limits
which ~ message families are forwarded.
- OSC streams to the
osc.stream_todestinations (host:port). Modeled reports get symmetric addresses —<prefix>/zone/<n>/level,<prefix>/scene,<prefix>/group/<id>/occupancy— and anything else falls back to<prefix>/monitor/<family>/<fields...>.osc.level_as_float(default true) picks the zone-level encoding (0–1 float or 0–255 int). - MQTT publishes to
<topic>/<monitor_prefix>/<family>/<fields...>(retained,monitor_prefixdefaults tomonitor). These are raw state topics for automations; no Home Assistant discovery is published for them.
What you'll need
- A computer to run it on. It's a single self-contained binary, so any Linux, macOS, or Windows machine works. This guide uses a Raspberry Pi as a cheap, low-power example and assumes Raspberry Pi OS / Raspbian 13 (Trixie); a Pi Zero is enough for serial-only control, while sACN, Art-Net, or Home Assistant need networking, so use a Pi Zero W (Wi-Fi) or any networked model. Adapt the steps to your platform if you run it elsewhere.
- A USB-to-serial adapter wired to the QSE-CI-NWK-E's serial terminals. The example config uses a common Prolific PL2303-style adapter; any 3.3 V / RS-232 adapter that matches your wiring will do. (Skip this if you're using the telnet transport instead — see below.)
- A GRAFIK Eye QS with the QSE-CI-NWK-E network/serial interface module.
- For lighting-board control: a lighting console or software that sends sACN/E1.31 or Art-Net over your network.
- For Home Assistant: a running MQTT broker (the Docker setup near the end of this guide includes one).
Serial or telnet? The QSE-CI-NWK-E offers both. Serial (a USB-to-serial adapter wired to the module) is the classic setup and needs no network between the Pi and the unit. Telnet talks to the module over your network instead — no wiring, and the Pi can live anywhere on the LAN. Pick whichever suits your install; the config covers both.
Installation
0. Get the Pi ready and download the code
If you're starting from a blank SD card, flash Raspberry Pi OS with the Raspberry Pi Imager. Before you write the card, open the imager's settings (the gear / "Edit settings" button) and:
- Set a username and password, and
- Enable SSH (so you can connect to the Pi from another computer).
Boot the Pi, then open a terminal on it — either directly with a keyboard and monitor, or from another computer over SSH:
ssh <user>@<pi-address>
Now install git and download this project:
sudo apt-get update
sudo apt-get install -y git
git clone https://github.com/GRMrGecko/lutron-control.git
cd lutron-control
Every command from here on is run from inside this lutron-control folder.
1. Install Go and build the program
This program is built with Go. Install the toolchain from the package manager:
sudo apt-get install -y golang
go version # confirm it printed a version
Now build the binary (from inside the lutron-control folder):
go build -o lutron-control .
This produces a lutron-control binary in the current folder. On a Pi Zero the
first build downloads dependencies and takes a few minutes; later builds are quick.
Faster: build on your computer instead. Go cross-compiles, so you can build the binary on a desktop and copy it to the Pi — no Go install on the Pi at all. From a checkout on your machine:
# 64-bit Pi (arm64): GOOS=linux GOARCH=arm64 go build -o lutron-control . # 32-bit Pi Zero / Zero W (ARMv6): GOOS=linux GOARCH=arm GOARM=6 go build -o lutron-control . # then copy it over: scp lutron-control <user>@<pi-address>:~/lutron-control/
2. Install the program, service, and config
Install the binary, create the config directory, drop your config in place, and install the systemd service that keeps it running and starts it on boot:
# The program (built in step 1):
sudo install -m 0755 lutron-control /usr/local/bin/lutron-control
# The config (locked down because it can hold your MQTT password):
sudo install -d /etc/lutron-control
sudo install -m 0600 config.example.yaml /etc/lutron-control/config.yaml
# The background service:
sudo install -m 0644 lutron-control.service /etc/systemd/system/lutron-control.service
sudo systemctl daemon-reload
Don't enable or start it yet — the config still has placeholder values. Fill them in first (step 3), then start it (step 4).
3. Fill in the config
Open the config in a text editor (nano is beginner-friendly):
sudo nano /etc/lutron-control/config.yaml
This file is a copy of config.example.yaml and is heavily
commented, so each setting explains itself. The important ones:
devices[].transport—serialortelnet.devices[].serial.device(serial transport) — which USB-serial adapter to use. Find yours by runningls -lah /dev/serial/by-id/and copying the matching path. Theby-idpath is stable across reboots, unlike/dev/ttyUSB0.devices[].serial.baud— must match the dipswitch setting on the QSE-CI-NWK-E.devices[].telnet.address(telnet transport) — the module'sIP:23. The login user isnwk(ornwk2); a password is only needed if one has been programmed on the unit.devices[].integration_idanddevices[].zones— set these to match your GRAFIK Eye. The integration ID is assigned in Lutron's programming; zones is how many dimmable zones your model has. Not sure of the integration ID? See the discovery tip below.sources[]— the control inputs. Delete thesacn/artnet/mqtt/oscsource blocks you don't want. For DMX, set theuniverseand channel layout; for MQTT, set your broker address,username/password, and the lights/zones; for OSC, set thelistenaddress andprefix.
Save and exit (Ctrl+O, Enter, then Ctrl+X in nano).
Don't know your integration IDs? Run discovery — it connects to each device in the config, prints its integration IDs, and exits:
sudo lutron-control --config /etc/lutron-control/config.yaml --discoverAdd
--verboseto any run to force debug logging to the console while you're setting things up.
Where the config lives: the program looks for it in this order —
--config PATH, then./config.yaml, then~/.config/lutron-control/config.yaml, then/etc/lutron-control/config.yaml(where the steps above put it). The systemd service points at the/etc/lutron-control/config.yamlcopy explicitly.
4. Start the service
Enable it (so it starts on every boot) and start it now:
sudo systemctl enable --now lutron-control
Check that it started cleanly (press Ctrl+C to stop watching the log):
journalctl -u lutron-control -f
When you update later — rebuild (step 1), reinstall the binary (step 2's first command), then restart:
sudo systemctl restart lutron-control
The service supports
Type=notifywith the watchdog — the program pings systemd while healthy, and systemd restarts it if it ever stops responding.
Home Assistant & MQTT (Docker)
If you want Home Assistant control, you need an MQTT broker for this program to
publish to. Here's how I run Home Assistant and the Mosquitto MQTT broker together
in Docker with docker compose.
Minimal compose.yaml:
services:
homeassistant:
container_name: home-assistant
image: homeassistant/home-assistant:stable
volumes:
- ./hass:/config
environment:
- TZ=America/Chicago
restart: always
network_mode: host
mqtt:
container_name: mqtt
image: eclipse-mosquitto
volumes:
- ./mosquitto:/mosquitto/config
restart: always
network_mode: host
network_mode: host lets Home Assistant find the broker and lets this control
program publish to it at 127.0.0.1:1883. Start it all with docker compose up -d.
Mosquitto config
Mosquitto (the MQTT broker) needs a config file and a password in the mounted
./mosquitto folder.
./mosquitto/mosquitto.conf:
per_listener_settings true
allow_zero_length_clientid true
listener 1883 0.0.0.0
allow_anonymous false
password_file /mosquitto/config/pwfile
acl_file /mosquitto/config/aclfile
./mosquitto/aclfile (gives the mqtt user full access):
user mqtt
topic readwrite #
Create the password file — use the same mqtt user and password you put in
config.yaml:
docker compose run --rm mqtt mosquitto_passwd -c -b /mosquitto/config/pwfile mqtt 'your-password'
docker compose restart mqtt
Home Assistant integration
In Home Assistant, add the MQTT integration (Settings → Devices & Services)
and point it at the broker: host 127.0.0.1, port 1883, and the mqtt
user/password.
With mqtt.discovery: true (the default in the example config), the lights and the
scene selector are announced to Home Assistant automatically and show up on their own
— no YAML editing required.
Recommended: hardware watchdog
The Raspberry Pi has a built-in hardware watchdog that can automatically reboot the Pi if it ever locks up. It's worth enabling for an always-on device like this.
Add this to /boot/firmware/config.txt (or /boot/config.txt on older images) under
the [all] section:
watchdog=on
Then uncomment RuntimeWatchdogSec in /etc/systemd/system.conf and set it:
RuntimeWatchdogSec=10s
Reboot to apply.