homebridge-luba
v0.5.7
Published
Control DALI devices via Lunatone's LUBA protocol from HomeKit.
Downloads
299
Maintainers
Readme
homebridge-luba
A Homebridge plugin that controls DALI lights through a Lunatone DALI-2 USB device using Lunatone's LUBA serial protocol, exposing each light to Apple HomeKit.
Features
- On / Off and Brightness for individual DALI control gear.
- Window coverings: DALI Jalousie modules (blinds / external solar screens) can be exposed as HomeKit Window Coverings with Open / Close / Stop.
- Automatic discovery of commissioned short addresses (0–63).
- DALI groups as single lights: every populated group is exposed as one HomeKit Lightbulb that dims all its members together (averaged brightness; on if any member is on). Members still appear individually unless you hide them.
- Live bus monitoring: changes made by other DALI controllers (wall dimmers, scene panels) appear in HomeKit within a couple of seconds, with a slow fallback poll as a safety net.
- Real device identity: each accessory's serial number, firmware/hardware version and model are read from the device (DALI memory bank 0) at discovery.
- Per-address and per-group name overrides and exclusions.
- Serial port auto-detection with manual override.
Prerequisite: your DALI devices must already be commissioned (short addresses assigned, and group membership set if you want groups) using the Lunatone DALI Cockpit. This plugin does not perform addressing/commissioning. Colour/tunable white (DT8) is not supported yet.
Installation
Install through the Homebridge UI (search for "Luba"), or:
npm install -g homebridge-lubaReleases are published from GitHub Actions with npm provenance — look for the
"Provenance" badge on the npm package page
to verify a given version was built from this repository's public source, or run
npm audit signatures after installing. See RELEASING.md for details.
Serial port access (Linux)
On Linux the DALI-2 USB appears as /dev/ttyACM0, owned by root:dialout
(mode 660). The user running Homebridge must be able to open it. Use either
of these one-time setups (substitute your Homebridge service user for
homebridge), then restart Homebridge.
Option 1 — add the Homebridge user to the dialout group (simplest):
sudo usermod -aG dialout homebridge
sudo systemctl restart homebridgeOption 2 — a udev rule scoped to the DALI-2 USB (no group change; grants access only to this device by its USB id):
# /etc/udev/rules.d/99-lunatone-dali.rules
echo 'SUBSYSTEM=="tty", ATTRS{idVendor}=="17b5", ATTRS{idProduct}=="0201", GROUP="homebridge", MODE="0660"' \
| sudo tee /etc/udev/rules.d/99-lunatone-dali.rules
sudo udevadm control --reload-rules && sudo udevadm trigger
sudo systemctl restart homebridgeThis is a standard requirement for any serial-based plugin. Without one of these, the plugin auto-detects the port but logs a "permission denied" error and controls nothing.
Configuration
Configure via the Homebridge UI, or add a platform block to config.json:
{
"platforms": [
{
"platform": "Luba",
"name": "Luba",
"port": "/dev/ttyACM0",
"baudRate": 38400,
"internalBusPower": false,
"busMonitor": true,
"pollIntervalSeconds": 60,
"defaultOffMode": "off",
"defaultOnMode": "lastActiveLevel",
"devices": [
{ "address": 0, "name": "Kitchen Ceiling" },
{ "address": 2, "name": "Hallway", "onMode": "recallMax", "offMode": "level0" },
{ "address": 5, "exclude": true }
]
}
]
}| Option | Description |
|--------------------|-------------|
| port | Serial port path (e.g. /dev/ttyACM0, COM3). Leave blank to auto-detect. |
| baudRate | Defaults to 38400 (the DALI-2 USB rate). |
| internalBusPower | Enable the DALI-2 USB's built-in 30 mA bus power supply at startup. See below. |
| busMonitor | Watch the bus for commands from other DALI controllers and update HomeKit. Defaults to true. See below. |
| pollIntervalSeconds | Safety-net re-query of every device's level (default 60; 0 disables polling). |
| removeMissingAfterDays | Grace period before a device that stops answering at startup is removed (default 7; 0 removes immediately). See Temporarily unavailable devices. |
| defaultOffMode | How HomeKit "off" is sent to all devices: off (DALI OFF, default) or level0. See below. |
| defaultOnMode | How HomeKit "on" is sent to all devices: lastActiveLevel (default), recallMax, recallMin, level, or onAndStepUp. See below. |
| defaultOnLevel | Brightness percent (1–100) used when the on mode is level. Defaults to 100. |
| hideGroupedMembers | Hide the individual member lights of any exposed group, so only the group tile appears. Defaults to false. |
| queryDeviceInfo | Read each device's identity (serial number, firmware/hardware version, GTIN) from DALI memory bank 0 at discovery and show it on the HomeKit accessory. Defaults to true. See Device identity. |
| devices | Optional per-address overrides — see Device overrides. |
| groups | Optional per-group overrides — see Group overrides. |
Device overrides
Discovered addresses not listed under devices use default names and
auto-detected types. Each entry supports:
| Option | Description |
|-----------|-------------|
| address | Required. The DALI short address (0–63) this entry applies to. |
| name | HomeKit accessory name. Defaults to DALI Device <address>. |
| manufacturer | HomeKit Manufacturer. Defaults to Unknown (a DALI device's maker can't be read from the bus). |
| model | HomeKit Model. Defaults to the device type plus the GTIN read from the device, e.g. DALI (DT6) · GTIN 9008390123456. |
| type | auto (default, use the DALI device type reported by the device), light (dimmable Lightbulb), switch (relay), or windowCovering (blind / solar screen — see below). Override only if a device misreports its type. |
| offMode | Per-device off command: default (use defaultOffMode), off, or level0. See below. |
| onMode | Per-device on command: default (use defaultOnMode), lastActiveLevel, recallMax, recallMin, level, or onAndStepUp. See below. |
| onLevel | Brightness percent (1–100) used when this device's on mode is level. Falls back to defaultOnLevel. |
| invert | Window coverings only. Set true to swap which direction is "open" if a screen moves the wrong way. |
| travelTimeSeconds | Window coverings only. How long (seconds) to show the screen "moving" so the Home app offers a Stop control. A movement indicator only — never used to estimate position. Default 20; 0 disables it (no Stop control). |
| exclude | Set true to hide this address from HomeKit entirely. |
Window coverings (DALI Jalousie)
Lunatone DALI Jalousie modules drive blinds and external solar screens. They
are relay interfaces — they only drive the motor up/down/stop, with no fractional
position feedback — and on the bus they identify as a plain dimmable light, so they
cannot be auto-detected. Set type to windowCovering for each such address
to expose it as a HomeKit Window Covering (otherwise it appears as a broken
light):
"devices": [
{ "address": 21, "type": "windowCovering", "name": "Screen Living" },
{ "address": 22, "type": "windowCovering", "name": "Screen Kitchen" },
{ "address": 27, "type": "windowCovering", "name": "Screen Office" }
]HomeKit shows Open / Close (the position slider snaps to fully-open /
fully-closed) plus a Stop control while the screen is moving. Open drives the
screen up, Close drives it down — if a screen runs the wrong way, add
"invert": true for that address.
Stopping mid-travel: the accessory tile is a toggle — tapping it while the
screen is moving sends a new target for the opposite end, so the screen
reverses (a tile tap can never mean "stop"). The plugin maps a HomeKit Stop
(the HoldPosition characteristic) to DALI OFF, but Apple's own Home app never
exposes a Stop button for window coverings — it only ever sends a target position,
never HoldPosition (a long-standing Home-app limitation). To stop a screen
mid-travel, use a third-party HomeKit app that does send it, such as Eve or
Controller for HomeKit; both show a Stop control while the screen is moving and
work against this plugin unchanged.
Although the Jalousie reports no fractional position, it does report its drive
direction, which the plugin reads via the bus poll to keep HomeKit in sync: the
correct open/closed state is restored on restart, and a screen moved by an external
wall switch is reflected in HomeKit within a poll cycle. Set travelTimeSeconds to
the screen's measured full open-to-close travel time so the Stop control stays
available for the whole movement. Proportional positioning and tilt are not yet
supported.
DALI groups
Every DALI group (0–15) that has at least one dimmable member is automatically exposed as a single HomeKit Lightbulb that controls the whole group. Group commands are sent as one native DALI group-addressed frame, so all members react together; each driver clamps the level to its own configured min/max, so a group of lights with different ranges still behaves sensibly.
A group has no single "actual level" to read back, so its state is synthesised from its members: brightness is the average of the members' current levels, and the group shows as on if any member is on. Member changes seen on the bus (including changes from other controllers) keep the group's reading up to date.
By default the members also stay visible as their own lights, so you can control
them individually or as a group. Set hideGroupedMembers: true to keep only the
group tiles. Group membership is read once at discovery (restart Homebridge after
changing it in the DALI Cockpit).
Group overrides
Discovered groups not listed under groups use a default name and the platform
on/off defaults. Each entry supports:
| Option | Description |
|-----------|-------------|
| group | Required. The DALI group number (0–15) this entry applies to. |
| name | HomeKit accessory name. Defaults to DALI Group <group>. |
| manufacturer | HomeKit Manufacturer. Defaults to Unknown. |
| model | HomeKit Model. Defaults to DALI Group. |
| offMode | Per-group off command: default (use defaultOffMode), off, or level0. |
| onMode | Per-group on command: default (use defaultOnMode), lastActiveLevel, recallMax, recallMin, level, or onAndStepUp. |
| onLevel | Brightness percent (1–100) used when this group's on mode is level. Falls back to defaultOnLevel. |
| exclude | Set true to hide this group from HomeKit entirely. |
Device identity
At discovery the plugin reads each device's DALI memory bank 0 (IEC 62386-102) and shows the real values on the HomeKit accessory's Information screen:
- Serial Number — the device's unique identification number (falls back to
DALI-<address>when the device doesn't report one). - Firmware and Hardware revision —
major.minor, when reported. - Model — the discovered DALI device type(s) plus the device's GTIN, e.g.
DALI (DT6) · GTIN 9008390123456. The GTIN identifies the product, not the maker. - Manufacturer — defaults to
Unknown: a DALI device's maker name can't be derived from the bus offline. Set themanufacturer(andmodel) override on adevices/groupsentry to show real names for gear you recognise.
This uses ordinary DALI reads (READ MEMORY LOCATION), reads degrade gracefully when
a device doesn't implement memory bank 0, and can be turned off entirely with
queryDeviceInfo: false (e.g. to speed up discovery).
On/Off command variants
Not all DALI control gear reacts the same way to the standard on/off commands,
so both are configurable — platform-wide via defaultOffMode/defaultOnMode,
or per device via offMode/onMode on a devices entry (the device setting
wins).
Off (offMode / defaultOffMode):
| Value | DALI command | When to use |
|----------|--------------|-------------|
| off | OFF | Default. Some drivers switch off instantly instead of fading. |
| level0 | Set level 0 (DAPC 0) | Fades to off using the driver's configured fade time. |
On (onMode / defaultOnMode):
| Value | DALI command | When to use |
|-------------------|--------------|-------------|
| lastActiveLevel | GO TO LAST ACTIVE LEVEL | Default. Some drivers ignore it and stay off — pick an alternative for those. |
| recallMax | RECALL MAX LEVEL | Always turn on at the device's maximum. |
| recallMin | RECALL MIN LEVEL | Always turn on at the device's minimum. |
| level | Set level (DAPC) | Turn on at a fixed brightness (onLevel/defaultOnLevel, in HomeKit percent); fades with the driver's fade time. |
| onAndStepUp | ON AND STEP UP | Turn on at minimum plus one step — works on drivers that ignore lastActiveLevel. |
Reacting to other DALI controllers
DALI is multi-master: wall dimmers, scene panels or other gateways on the same
bus can change a light without HomeKit knowing. With busMonitor enabled
(the default), the plugin listens to the bus and, when it sees a command that
changes light output, re-reads the affected device's actual level and updates
HomeKit — typically within a couple of seconds, with follow-up reads while a
fade is still running. Group and broadcast commands are handled too (each
device's group membership is read at discovery).
The plugin's own commands are filtered out at the interface, so HomeKit actions don't trigger redundant reads.
As a safety net, every device is also re-read periodically
(pollIntervalSeconds, default every 60 s) in case an event was missed. Set it
to 0 to disable polling, or set busMonitor: false to rely on polling alone.
Temporarily unavailable devices
Discovery runs once at startup. If a device fails to answer then — because it is powered down, mid-firmware-update, or briefly off the bus — it is not removed straight away, which would otherwise discard the room, name and automations you set up for it in the Home app. Instead the accessory is kept and shown as "No Response" in HomeKit, and a warning is logged.
Recover the device and restart Homebridge (or the child bridge) and it is restored
automatically. If it is still missing removeMissingAfterDays days later (default
7), it is finally unregistered. Set removeMissingAfterDays: 0 to remove missing
devices immediately on the next restart instead.
This grace period applies only to devices that vanish from the bus. A device or
group you deliberately remove from HomeKit — by setting exclude: true, or hiding a
group member via hideGroupedMembers — is unregistered immediately on the next
restart, not parked as "No Response".
DALI bus power
DALI control gear needs bus voltage to respond. The DALI-2 USB has a built-in, switchable 30 mA supply that is off by default, so with no power the plugin discovers zero devices and logs a bus-voltage error.
- If no separate DALI power supply is on the line, set
internalBusPower: trueto let the adapter power the bus. - If you already have an external DALI power supply, leave it
false. Never run two bus power supplies at once.
The setting is authoritative in both directions: the adapter remembers the bus-power
bit across restarts, so internalBusPower: false actively turns the integrated
supply off at startup (undoing a previous true), not merely "don't enable it".
Child bridge
Running this plugin in a child bridge is recommended, so its exclusive access to the serial port is isolated from the rest of Homebridge.
Debug logging
The plugin emits detailed debug traces of its asynchronous messaging — serial request/response correlation, the bus monitor's debounce/settle/poll cycle, discovery, and the optimistic state guard — to help diagnose timing-sensitive issues (e.g. a light not tracking another controller, or a slider snapping during a fade). These are silent during normal operation; enable them by turning on Debug Mode for the plugin's child bridge (Homebridge UI → the plugin's bridge settings), which scopes the extra output to this plugin alone rather than all of Homebridge.
How it works
The plugin is layered so the two protocols stay independent and the logic is testable without hardware:
platform / accessory → DaliGateway → { dali/codec, luba/transport } → { luba/frame, luba/link }luba/link— raw serial bytes (SerialLinkinterface; node-serialport impl).luba/frame— pure LUBA frame (de)serialization ('Y'guard + XOR checksum).luba/transport— sends frames, parses the RX stream, correlates query responses.dali/codec— pure DALI 16-bit frame encoding and decoding (DAPC / command).gateway— joins DALI ops to LUBA transport; owns discovery, per-device control handles, and the bus monitor that follows other controllers' changes.
Development
npm install
npm run build
npm test # unit tests, no hardware required
npm run lintTo probe a real device without Homebridge:
npx tsx scripts/probe.ts /dev/ttyACM0
# add --enable-bus-power if the bus has no external power supply:
npx tsx scripts/probe.ts /dev/ttyACM0 --enable-bus-power
# or --disable-bus-power to turn the integrated supply back off:
npx tsx scripts/probe.ts /dev/ttyACM0 --disable-bus-power
# watch and decode DALI frames sent by other controllers on the bus:
npx tsx scripts/probe.ts /dev/ttyACM0 --monitorChangelog
See CHANGELOG.md for the version-by-version history.
