npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

homebridge-luba

v0.5.7

Published

Control DALI devices via Lunatone's LUBA protocol from HomeKit.

Downloads

299

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-luba

Releases 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 homebridge

Option 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 homebridge

This 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 the manufacturer (and model) override on a devices/groups entry 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: true to 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 (SerialLink interface; 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 lint

To 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 --monitor

Changelog

See CHANGELOG.md for the version-by-version history.

License

Apache-2.0