lutron-control/README.md
2026-06-28 08:32:22 -05:00

445 lines
18 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 01, or int 0255) |
| `<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 14, 2 scenes 516) |
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 (01 float or 0255 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.