# 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](#how-it-works) — the concepts behind devices, sources, and control - [What it does](#what-it-does) - [Zones and scenes](#zones-and-scenes) - [Arbitration](#arbitration) - [OSC control](#osc-control) - [Monitoring feedback](#monitoring-feedback) - [What you'll need](#what-youll-need) — hardware and prerequisites - [Installation](#installation) — build, install, configure, and run - [Home Assistant & MQTT (Docker)](#home-assistant--mqtt-docker) - [Recommended: hardware watchdog](#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: ```yaml 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 `/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. ```yaml 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. ```yaml 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 | | --- | --- | --- | | `/zone//level` | `f` or `i` | set zone `` level (float 0–1, or int 0–255) | | `/zone//raise` | — | start raising zone `` | | `/zone//lower` | — | start lowering zone `` | | `/zone//stop` | — | stop raising/lowering zone `` | | `/scene` | `i` | activate scene `` | | `/scene/off` | — | activate the scene-off look | | `/shade//` | — | drive shade column `` (`open`/`close`/`preset`/`raise`/`lower`/`stop`) | | `/lock/zone` | `i` | zone lock (0 off, 1 on) | | `/lock/scene` | `i` | scene lock (0 off, 1 on) | | `/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](#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_to` destinations (`host:port`). Modeled reports get symmetric addresses — `/zone//level`, `/scene`, `/group//occupancy` — and anything else falls back to `/monitor//`. `osc.level_as_float` (default true) picks the zone-level encoding (0–1 float or 0–255 int). - **MQTT** publishes to `///` (retained, `monitor_prefix` defaults to `monitor`). 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](https://www.raspberrypi.com/software/). 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: ```bash ssh @ ``` Now install git and download this project: ```bash 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: ```bash sudo apt-get install -y golang go version # confirm it printed a version ``` Now build the binary (from inside the `lutron-control` folder): ```bash 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: > ```bash > # 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 @:~/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: ```bash # 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): ```bash sudo nano /etc/lutron-control/config.yaml ``` This file is a copy of [`config.example.yaml`](config.example.yaml) and is heavily commented, so each setting explains itself. The important ones: - **`devices[].transport`** — `serial` or `telnet`. - **`devices[].serial.device`** (serial transport) — which USB-serial adapter to use. Find yours by running `ls -lah /dev/serial/by-id/` and copying the matching path. The `by-id` path 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's `IP:23`. The login user is `nwk` (or `nwk2`); a password is only needed if one has been programmed on the unit. - **`devices[].integration_id` and `devices[].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 the `sacn`/`artnet`/`mqtt`/`osc` source blocks you don't want. For DMX, set the `universe` and channel layout; for MQTT, set your broker address, `username`/`password`, and the lights/zones; for OSC, set the `listen` address and `prefix`. 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: > ```bash > sudo lutron-control --config /etc/lutron-control/config.yaml --discover > ``` > Add `--verbose` to 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.yaml` copy explicitly. ## 4. Start the service Enable it (so it starts on every boot) and start it now: ```bash sudo systemctl enable --now lutron-control ``` Check that it started cleanly (press `Ctrl+C` to stop watching the log): ```bash journalctl -u lutron-control -f ``` When you update later — rebuild (step 1), reinstall the binary (step 2's first command), then restart: ```bash sudo systemctl restart lutron-control ``` > The service supports `Type=notify` with 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`: ```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`**: ```bash 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.