445 lines
18 KiB
Markdown
445 lines
18 KiB
Markdown
# 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 `<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.
|
||
|
||
```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 |
|
||
| --- | --- | --- |
|
||
| `<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](#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 — `<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_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 <user>@<pi-address>
|
||
```
|
||
|
||
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 <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:
|
||
|
||
```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.
|