# node-red-contrib-dmx-for-ha

**Professional DMX lighting control for Home Assistant via Node-RED and MQTT.**

Built by a lighting integrator, for lighting integrators — and anyone else who needs serious DMX control inside Home Assistant.

Place a node, fill in the settings, deploy. Your DMX fixture appears in Home Assistant automatically — ready to use in automations, dashboards, and scenes.

No YAML. No custom MQTT discovery config. No external wiring inside Node-RED.

---

## Who this is for

This package was designed for **professional AV and lighting integrators** working on large residential and commercial installations. If any of these sound familiar, this was built for you:

- You have hundreds of DMX fixtures that each need individual HA entity management
- You commission remotely — physically pressing every button on a large site is not practical
- Your client's lights must come back to the correct state after any power cycle, every time
- Your automation logic lives in HA, not hardcoded in the lighting controller
- You have multiple DMX universes and controllers in a single zone
- You've stood in a roof cavity at 11pm wondering why channel 47 isn't responding

It also works perfectly for advanced home users who want proper DMX integration rather than a workaround.

---

## The architecture

Node-RED acts as the **virtual lighting desk** — it owns the DMX channels, manages transitions, and handles hardware communication. Home Assistant is the **automation and UI layer** — it sees clean light entities and knows nothing about DMX channels, universes, or controllers.

```
Home Assistant          Node-RED               DMX Hardware
──────────────          ────────               ────────────
Automations      ←──→  ha-mqtt-dmx     ──→    EtherTen /
Dashboards              ha-mqtt-group           DMX decoder /
Scenes                  ha-mqtt-relay           MQTT bridge
Voice control           ha-mqtt-button  ←──    Wall buttons
                        ha-mqtt-pir     ←──    PIR sensors
```

This separation means:
- **HA stays clean** — light entities behave like any other HA light
- **NR handles complexity** — DMX addressing, gamma correction, transitions, effects
- **Hardware is abstracted** — swap controllers without touching HA config
- **Remote commissioning** — every physical device has a software mirror in HA, testable from anywhere

---

## What this package does

Bridges the gap between DMX lighting hardware and Home Assistant.

Most DMX implementations require either expensive proprietary hardware or complex custom integrations. This package gives you drop-in Node-RED nodes that handle everything:

- Full Home Assistant MQTT discovery — entities appear automatically in HA
- RGBW, RGBWW, RGB, Colour Temperature, Brightness, and On/Off colour modes
- Gamma-corrected DMX output with configurable floor and limiter
- Smooth transitions with configurable ON and OFF fade times
- Effects engine — strobe, rainbow, fire, flicker, twinkle, police and more
- Group control — virtual group entities fan commands to member fixtures
- State persistence across reboots — survives power cycles and HA updates
- Wall button and PIR motion sensor integration with remote commissioning support
- 230V relay switching
- DMX channel conflict detection — warns on duplicate channel assignments
- Per-node DMX floor — prevents low-value flicker on hardware that needs it
- Debug mode per node — 12hr auto-disable, safe to leave in production flows

---

## Nodes included

| Node | Palette label | Colour | Purpose |
|---|---|---|---|
| `ha-mqtt-site` | *(config)* | — | Site config — MQTT broker, Site ID, discovery prefix, target profile |
| `ha-mqtt-config` | *(config)* | — | Zone config — references ha-mqtt-site, sets zone, floor, DMX defaults |
| `ha-mqtt-area` | *(config)* | — | Area + sub-area registry for the flow |
| `ha-mqtt-system` | System Control | Blue | System control panel — discovery, debug, status, runtime overrides |
| `ha-mqtt-dmx` | DMX | Yellow | Single DMX fixture |
| `ha-mqtt-dmx-group` | DMX Group | Gold | Virtual group — fans commands to member fixtures |
| `ha-mqtt-relay` | Relay | Red | 230V relay switching |
| `ha-mqtt-button` | Button | Green | Wall button receiver |
| `ha-mqtt-pir` | PIR | Purple | Motion sensor receiver |
| `lists` | *(config)* | — | Configurable entry list for device types, button positions, PIR types |

Node colours match the cable colour convention used on electrical plans.

---

## Requirements

- **Node-RED** 5.0.0 or later
- **Home Assistant** with MQTT integration configured
- **Mosquitto** (or any MQTT broker) accessible from Node-RED
- **Node.js** 24.0.0 or later
- **A `disk_values` context store** in Node-RED's `settings.js` — required
  for fixture/relay/group state to survive Node-RED restarts. Without it,
  Node-RED logs `Unknown context store 'disk_values'` once at startup and
  state recovery silently degrades to memory-only (everything falls back
  to its Default State after a restart). Add to `settings.js`:

  ```js
  contextStorage: {
    default: { module: 'memory' },
    disk_values: { module: 'localfilesystem' }
  },
  ```

  On the Home Assistant Node-RED addon, edit
  `/addon_configs/a0d7b954_nodered/settings.js` (keep any existing
  entries in the block) and restart the addon — the startup log should
  then show `Context store : 'disk_values' [module=localfilesystem]`.

---

## Installation

### Via HA Node-RED Add-on config

In HA → Settings → Add-ons → Node-RED → Configuration, add to the packages list:
```
node-red-contrib-dmx-for-ha
```
Restart the add-on. Nodes appear in the palette under **DMX for HA**.

### Via Node-RED Palette Manager

Node-RED → Menu → Manage Palette → Install → search `node-red-contrib-dmx-for-ha`

### Via npm

```bash
cd ~/.node-red
npm install node-red-contrib-dmx-for-ha
```

---

## ⚠️ Required: Configure Node-RED context storage

This package stores fixture state so it survives Node-RED restarts. A **disk context store** must be configured in Node-RED.

**Without this, fixture state resets to default every time Node-RED restarts.**

### How to configure it

Find your Node-RED settings file:

**HA add-on** — the file is inside the add-on container, not accessible via Samba. Use Studio Code Server (VS Code in HA) and open a terminal:
```bash
code /addon_configs/a0d7b954_nodered/settings.js
```
The `a0d7b954` is the NR add-on ID — it's the same on every HA install.

**Standard install:**
```
~/.node-red/settings.js
```

Add or update the `contextStorage` section:

```javascript
contextStorage: {
    default:     { module: 'memory' },
    disk_values: {
        module: 'localfilesystem',
        config: {
            dir: '/config/node-red/context',
            flushInterval: 5
        }
    },
},
```

Restart Node-RED. Verify in the startup log:
```
Context store  : 'disk_values' [module=localfilesystem]
```

---

## Quick start

**Fastest path:** Node-RED menu → Import → Examples → **node-red-contrib-dmx-for-ha** → "Quick Start". This gives you a placeholder broker (fill in your real host/port — the only thing you *must* configure), a Site node already linked to it, and five ready-to-select Lists (L / P / F / BTN / PIR) so every node type has a sensible option in its dropdown from the very first fixture you drop onto the canvas. A Zone is deliberately *not* included — that's the one step that genuinely needs your input, since it's where your real broker connection actually gets used.

The steps below walk through the same thing by hand, useful if you want to understand what the example is doing or you're setting up a second site.

### 1. Create a site node

1. Open the Node-RED menu → Configuration Nodes → Add → **ha-mqtt-site**
2. Fill in:
   - **Site Label** — a friendly name shown in config dropdowns e.g. "My House"
   - **Site ID** — short slug for MQTT topics e.g. `home` (no spaces)
   - **MQTT Broker** — select your existing broker config
   - **Target Profile** — `homeassistant` (default) or `standalone` (no HA discovery)
   - Leave discovery settings as defaults unless your HA MQTT integration is customised
3. Click **Add**

### 2. Create a zone config node

1. Drag a **DMX** node onto the canvas and open its editor
2. Click **+** next to the Zone field to add a new **ha-mqtt-config**
3. Fill in:
   - **Site** — select the ha-mqtt-site node you just created
   - **Name** — a friendly name e.g. "Master Zone"
   - **Zone** — physical zone e.g. `Zone1`, `ZoneA`. Used in MQTT topics.
   - **Floor** — physical level e.g. `GF`, `1F` (optional)
   - **DMX Floor** — minimum DMX value sent (default 3). Prevents flicker at low levels.
   - **Default ON/OFF transition** — fade time when HA sends no transition (default 1s)
4. Click **Add**

### 3. Add a DMX fixture

Fill in the node editor:
1. **Fixture ID** — Prefix (`L`), Plan ID, optional channel letter e.g. `L` `992` `-A`
2. **Colour Mode** — RGBW, RGB, CCT etc.
3. **Device Type** — e.g. Downlight, Strip light
4. **Situation** — describes where the fixture is relative to its area:

   | Situation | Example result |
   |---|---|
   | `in` | Downlight **in** Bedroom 1 |
   | `at` | Spotlight **at** Entry Door |
   | `near` | Strip light **near** Kitchen Bench |
   | `above` | Flood **above** Garage Door |
   | `below` | Step light **below** Handrail |
   | `outside` | Rail light **outside** Media Room |
   | `throughout` | Downlight **throughout** Living Area |

5. **Config** — select your config node
6. **Area** and **Sub-Area** — location on the property
7. **DMX Channels** — channel numbers matching your fixture's DMX start address
8. **Controller** and **Universe** numbers
9. **DMX Floor** — leave blank to use the config node default, or set a value to override for this fixture only
10. **Discovery Mode** — Enabled, Hidden, or Disabled

Deploy — the fixture appears in HA automatically.

---

## DMX Output Shaping

Four fields control how raw brightness values are shaped into DMX channel values:

### ⬆ DMX ON Floor Value
Sets the **absolute minimum channel value** sent when a fixture is intentionally ON. Prevents flickering on LED decoders that are unstable at values 1 or 2.

```
value = 0         → sends 0       (OFF — always honoured)
0 < value < floor → sends floor   (snapped up)
value >= floor    → sends value   (unchanged)
```

Enforced at the output stage — no code path can bypass it. **Config node** sets the site-wide default (e.g. 3). **DMX node** can override per fixture — blank = inherit from config.

### ⬇ DMX ON Roof Value
Caps the **maximum channel value** sent (0–255). Use to prevent over-driving fixtures or to limit output for safety. Default: 255.

### γ Gamma Correction
If the gamma brightness curve calculates to **exactly zero** but the fixture is intentionally ON, output this value instead. Prevents the fixture going completely dark at the very bottom of the dimming curve. Set to 0 for pure gamma (no rescue).

### ⤴ Brightness ON bump
The DMX value the fixture **jumps to when first turning ON**, before transitioning to the requested brightness. Useful for fixtures that need an initial kick to strike. 0–255.

---

## ha-mqtt-system — System Control Node

Single control surface for all system-level operations. Replaces the inject-node wall on the DEBUG tab.

Place one on the canvas, connect it to a `ha-mqtt-site` node, deploy — then open the node editor to control the whole site.

### Sections

**Discovery** — Add or remove HA discovery entities by zone and type. Wire to MQTT out or use directly.

**Debug** — Enable/disable debug mode across a zone at runtime without redeploying. Auto-disables after 12 hours.

**Enable / Disable** — Take nodes offline temporarily without removing from HA.

**Runtime Overrides** — Live-tune DMX floor, transition rate, and ON/OFF transition times across a zone. Changes revert on redeploy unless also set in Persistent Overrides.

**Status / Health** — Request a status report from all nodes in a zone. Responses arrive on the output port — wire to a debug node.

**Persistent Overrides** — Site-wide settings broadcast on every deploy, overriding config node defaults. Blank = use each config node's own value. The Transition Rate Limit is the most useful override on large installs.

### System control topic

All commands publish to `{siteId}/system/control`. The node also accepts `msg` input — send `msg.payload = {"cmd": "...", ...}` for advanced use.

```json
{"cmd": "add",           "zone": "Master", "type": "dmx"}
{"cmd": "remove",        "zone": "all",    "type": "all"}
{"cmd": "debug",         "zone": "Master", "enable": true}
{"cmd": "floor",         "zone": "all",    "value": 5}
{"cmd": "rateLimit",     "zone": "Master", "value": 0.5}
{"cmd": "onTransition",  "zone": "all",    "value": 1}
{"cmd": "offTransition", "zone": "all",    "value": 2}
{"cmd": "disable",       "zone": "ZoneA",    "type": "dmx"}
{"cmd": "enable",        "zone": "ZoneA",    "type": "all"}
{"cmd": "status",        "zone": "all"}
```

---

## lists — Configurable List Node

Generic configurable list for device types, button positions, PIR types, or any future use. One instance per use case.

### Standard list vs custom entries

The standard list lives in the package code and updates automatically with new npm releases. Your custom entries are stored in your flow and are never touched by updates.

- **Unchecked** (Use my own custom entries) — always serves the standard list. New entries from npm updates appear automatically.
- **Checked** — serves your customised list. Standard entries you removed stay hidden. Your additions are preserved.
- Unchecking never deletes your changes — you can recheck at any time.
- **Reset to standard list** (with confirmation) is the only destructive action.

### Prefix convention

| Prefix | Contents |
|---|---|
| `L` | Lighting circuit fixture types |
| `P` | Dedicated power circuit types |
| `F` | Fan types |
| `BTN` | Button positions |
| `PIR` | PIR sensor types |
| Custom | Starts empty — build from scratch |

---

## DMX Group Node

Appears as a single light entity in HA. Commands fan out to all member fixtures.

### Two ways to define members

**Method A — Wires (v1, still supported):**
```
[DMX Group LG-992]
      │ Link output
      ├──→ [DMX L-992-A]
      ├──→ [DMX L-992-B]
      └──→ [DMX L-992-C]
```

**Method B — Member list (v2, recommended):**

Add fixture IDs directly in the group node editor. No wires needed. Commands are forwarded internally via the Node-RED message bus — no extra MQTT traffic.

```
Members:
  local   L-992-A     → forwarded via RED.nodes.getNode() internally
  local   L-992-B     → forwarded via RED.nodes.getNode() internally
  local   P-149       → relay node, forwarded internally
  remote  site1/Zone1/group/cmd  → published via MQTT for cross-instance groups
```

Both methods can be used together. If a fixture appears in both wires and the member list it will receive the command twice — avoid this.

### Member types

| Type | Value | How it works |
|---|---|---|
| `local` | Fixture ID e.g. `L-992-A` | Resolved via fixture registry → `RED.nodes.getNode()` |
| `remote` | MQTT topic e.g. `site1/Zone1/group/cmd` | Published via MQTT with loop-detection envelope |

### Duplicate member protection

If the same fixture ID appears more than once in the member list, the duplicate entry is **skipped with a node warning** — the remaining members still receive the command. An orange status is shown on the node. The editor also prevents saving with duplicates.

### Loop detection

The node tracks the cascade path and blocks circular references. Works across wired groups, member list groups, and cross-instance remote groups. Max depth defaults to 10.

### Node status

| Colour | Meaning |
|---|---|
| Green dot | Healthy — all members resolved |
| Yellow ring | One or more members not found in registry |
| Orange ring | Duplicate member detected — group blocked |
| Red dot | Loop detected |
| Red ring | Broker disconnected |

### Naming convention

`L-992-A`, `L-992-B`, `L-992-C` group naturally under `LG-992`. The `LG` prefix is fixed.

---

## Discovery Modes

| Mode | Behaviour |
|---|---|
| **Enabled** | Discovered and visible in HA dashboard |
| **Hidden** | Discovered and usable in automations, not auto-placed in dashboard |
| **Disabled** | Not discovered — entity does not exist in HA |

Use **Disabled** for fixtures not yet installed or wired.
Use **Hidden** for fixtures installed but not ready for the homeowner.

---

## Canvas button

Each node has a clickable button on the right side of the canvas node. It toggles between `device:remove` and `device:add`:

- First press → `device:remove` — cleanly removes entity from HA
- Second press → `device:add` — re-announces to HA

A toast notification confirms which action fired.

Typical workflow: Remove → update node settings → Deploy → node auto-discovers with new settings.

---

## System control topic

All nodes subscribe to a system control topic for **bulk add/remove during commissioning**. No wiring needed — publish one MQTT message and all matching nodes respond instantly.

**Topic:** `{siteId}/system/control` (e.g. `siteA/system/control`)

**Payload:**
```json
{"cmd": "remove", "zone": "all",    "type": "all"}
{"cmd": "add",    "zone": "Master", "type": "dmx"}
{"cmd": "remove", "zone": "ZoneA",    "type": "button"}
```

| Field | Values | Description |
|---|---|---|
| `cmd` | `add`, `remove` | Add or remove HA discovery |
| `zone` | `all`, or any zone name e.g. `Zone1`, `ZoneA` | Matches config node Zone field — case insensitive |
| `type` | `all`, `dmx`, `group`, `button`, `pir`, `relay` | Node type filter |

**Commissioning workflow:**
```
ADD Master DMX    → all lights appear in HA → test each light
REMOVE Master DMX → wipe and fix any wrong names/channels
ADD Master DMX    → rediscover clean
```

---

## Wall buttons

Creates two HA entities per button:

- **`binary_sensor.s_10_a`** — physical state. ON = pressed/held, OFF = released.
- **`button.s_10_a_btn`** — momentary trigger. Simulates a physical press from the HA UI.

The `button` entity gives you a **virtual commissioning panel** — every physical wall button has a software mirror in HA that you can trigger from anywhere without being on site.

The `button` domain in HA is intentionally stateless — it will never show an active state. For visual feedback always use the `binary_sensor` entity.

Letters `I` and `O` are never used as suffixes — they look too similar to `1` and `0`.

---

## PIR sensors

Goes **offline** in HA during a configurable warm-up period after NR starts — prevents false triggers during reboots. Default warm-up: 120s.

---

## Fixture ID conventions

| Prefix | Meaning | Example |
|---|---|---|
| `L` | Light (DMX) | `L-992-A` |
| `P` | Power (Relay) | `P-51` |
| `LG` | Light Group | `LG-992` |
| `S` | Sensor (Button/PIR) | `S-10-A` |

Entity IDs in HA are locked to the fixture ID and survive friendly name changes.

---

## Retain discovery — why the default is false

If you search "MQTT discovery retain" you will find HA community posts recommending `retain=true`. **This advice is correct for ESP and battery-powered IoT devices — but not for Node-RED.**

Node-RED sits on the same server stack as HA. When HA restarts, NR reconnects and re-discovers every device automatically. With `retain=true`, ghost entities persist in HA even when NR is down — clients see lights that don't work and can't tell why.

With `retain=false` (default): if NR stops, entities disappear. Clients see a fault immediately. This is the correct behaviour for a professionally installed system.

| Setting | Recommended for |
|---|---|
| `false` (default) | Professional installs — clear fault indication |
| `true` | Standalone IoT where entity persistence matters more than fault clarity |

---

## Transition rate tuning

The config node has two global transition settings:

### Transition rate limit

Controls the tick rate multiplier for all transitions and effects:

```
ticksPerSec (per node) × rateLimit (config) = effectiveTicks/sec
```

| Rate Limit | Effective ticks/sec | Use case |
|---|---|---|
| 1.0 | 31 | Small deployment, fast hardware |
| 0.5 | 15 | Medium deployment, balanced |
| 0.25 | 7 | Large deployment, light load |
| 0.1 | 3 | Maximum scale, minimal load |

Reduce `transitionRateLimit` if you see MQTT broker lag, NR event loop warnings, or sluggish HA response during scene changes.

### Default transition times

| Field | Default | Description |
|---|---|---|
| Default ON transition | 1s | Fade time when HA sends ON with no transition specified |
| Default OFF transition | 1s | Fade time when HA sends OFF with no transition specified |

Set to `0` for instant snap on/off.

---

## Context store — finding your files

The NR add-on runs in its own Docker container. Context store files are **not** accessible via Samba.

| Location | Path |
|---|---|
| NR add-on config | `/addon_configs/a0d7b954_nodered/` |
| Context store files | `/addon_configs/a0d7b954_nodered/nodeRED/context_stores/context/` |

Use **Studio Code Server** add-on to access these files. One `.json` file per node, named by node ID.

### Recommended config.js settings

```javascript
contextStorage: {
    memory:      { module: 'memory' },
    disk_values: {
        module: 'localfilesystem',
        config: {
            dir: '/config/nodeRED/context_stores',
            flushInterval: 5
        }
    },
    default:     { module: 'memory' },
},
```

Use an **absolute path** starting with `/config/`. A relative path will silently write to the wrong location.

`flushInterval: 5` writes to disk every 5 seconds instead of the default 30 — reduces the window of data loss on unexpected shutdown.

---

## Wired command source — NRHA integration

DMX and Relay nodes support an alternative command path for users of the
[node-red-contrib-home-assistant-websocket](https://github.com/zachowj/node-red-contrib-home-assistant-websocket) package.

Set **Command source → Wired input** in the ⚙ Options section of a DMX or Relay node.
In this mode the MQTT command subscription is skipped — commands arrive via the NR wire input instead.
HA discovery and state reporting still use MQTT as normal.

### Typical wiring

```
[NRHA call service node] ──→ [ha-mqtt-dmx — wired mode]
```

### msg format

| Field | Value | Notes |
|---|---|---|
| `msg.topic` | `L-970` or `light.l_970` | Fixture ID filter — non-matching messages silently dropped |
| `msg.payload.state` | `"on"` / `"off"` | Case insensitive |
| `msg.payload.brightness` | `0–255` | Optional |
| `msg.payload.color` | `{r,g,b,w,ww}` or `rgb_color:[r,g,b]` | Optional — NRHA format accepted |

Omit `msg.topic` to send to all wired DMX nodes on the wire — use with care on large flows.

### What you still get in wired mode

All the DMX engine features remain active — gamma correction, transitions, effects,
DMX floor, group cascading, state persistence, and HA discovery. The only change is
where commands arrive from.

### ⚠ Feedback loop warning

Do not wire a **state-changed** node back into a wired-mode DMX node.
This node publishes its own state via `pubState()` after every command — a state-changed
listener on the same entity will fire and retrigger the node, creating a loop.
Use **call service** instead, which fires only on explicit user action.

### Combinations

| Input | Target | Works? |
|---|---|---|
| NRHA call service | DMX wired | ✅ Intended use case |
| NRHA events: all | DMX wired | ✅ With topic filter |
| NR inject node | DMX wired | ✅ Useful for testing |
| NRHA state-changed | DMX wired | ❌ Feedback loop |
| HA UI button | ha-mqtt-button | ❌ Wrong direction — ha-mqtt-button is for hardware input |
| Wired mode + standalone profile | DMX wired | ✅ Pure NR→DMX, no HA needed at all |

---

## Troubleshooting

**Nodes don't appear in palette** — Restart Node-RED. Check log for errors.

**Fixture doesn't appear in HA** — Check broker connection. Check canvas status. Use canvas button to manually trigger `device:add`.

**State resets on restart** — Configure disk context storage. Check log for `disk_values` store confirmation.

**Node shows "Broker disconnected"** — Check config node broker settings.

**Node shows "Disabled — not discovered"** — Change Discovery Mode to Enabled or Hidden and deploy.

**PIR stuck offline** — Warm-up timer running. Wait for configured warm-up period.

**Group member not found (yellow warning)** — Fixture ID not in registry. Check the fixture node is deployed and registered. Command still fires to all other members.

**Group command blocked (orange warning)** — Duplicate member in the list. Open the group node editor and remove the duplicate.

---

## Version history

| Version | Changes |
|---|---|
| 0.9.25 | Docs: the `disk_values` context-store requirement is now documented in Requirements (with the exact `settings.js` block and Home Assistant addon path). Discovered on a real installation: without it, Node-RED logs `Unknown context store 'disk_values'` and all restart-state recovery silently degrades to memory-only. No code changes — v0.9.24's behaviour is unchanged. |
| 0.9.24 | **Pre-v1 hardening batch** (first release from the clean repo — seven stale pre-0.9.14 profile files that had been riding along in every tarball since the Pragiom rename are gone: etherten_v1/v2, bedrock_v1, and the four legacy button/PIR profile files). Fixed: disabled DMX Group nodes threw a ReferenceError during discovery AND the editor silently discarded the group's Discovery Mode value on every save; fixture-ID registry entries leaked when nodes were deleted (all 5 node types — the phantom "DUPLICATE FIXTURE ID" class), relay delete also leaked its relay-number claim; Relay and Group state recovery after a full Node-RED restart always fell back to Default State (disk store read a key that was never written — DMX was unaffected); Button binary_sensor discovery was hardcoded retained, ignoring the site's "Retain discovery" setting; System Control's offTransition runtime/persistent overrides did nothing (written but never read); duplicate-ID warnings printed a literal `$_{...}` instead of the offending node id. Changed: the never-functional `custom` controller profile is no longer selectable in any dropdown (profiles kept in place, made fail-safe for legacy flows; proper custom-template support is on the roadmap); the Relay "MQTT segment" field is hidden (a no-op with every built-in profile — returns when custom profiles are wired); Group effect list trimmed to the ten effects the engine actually implements; "Show effects" now defaults OFF on DMX and Group (cleaner HA menus — enable per-entity where wanted) and no longer gates effect *commands* on Relay: effects sent directly over MQTT (e.g. from HA automations via mqtt.publish) always work, the checkbox only controls HA advertisement; transitions are forced off for On/Off colour mode (binary fixtures never fade) and the editor hides the option for that mode; lists custom-entry merging no longer depends on the Node 22+ `Set.difference` API. Examples: the Quick Start flow is renamed to "Quick Start" and now ships all five default Lists (the F list was missing). Docs: `profiles/README.md` rewritten to match the current per-node-type profile contracts and two-step registration. |
| 0.9.23 | new: `bootstrap_site.py` now also creates all five standard Lists config nodes (L/P/F/BTN/PIR) on every fresh site bootstrap, and `generate_area.py` automatically wires the matching list into every generated node's list field — no manual list creation needed on a new install. UI reorg across all 5 node types: the Fixture/Device/Position/PIR List field moved below the Advanced section (always visible), "Ignore channel conflict warnings" and "Debug output" moved inside Advanced (collapsed by default), and Relay/Button/PIR gained an Advanced section for the first time. Button Hold Time default increased from 0.5s to 1s (HTML default and runtime fallback updated together). |
| 0.9.20–0.9.22 | Live-commissioning fix batch, published incrementally: relay command topic had a stray `/command` suffix; DMX Group "Members" array entries were written as plain strings instead of `{type, value}` objects; dead legacy controllerProfile fallback defaults removed across all node generators; PIR status ring never cleared (no hold-time timer — added); Group node registry HTTP endpoint read a property that was never set; `showEffects`/`transitions` runtime checks tightened from `!== false` to explicit `=== true`; PIR generator cableColour/cableColor spelling mismatch; System Control zone dropdowns now scan the real zone config nodes instead of a hardcoded list, and the System Control HTTP endpoint gained an authentication check; empty "Lights" sub-box no longer generated for a group-of-groups. |
| 0.9.19 | UX fix: the "Fixture List" / "Device List" field was hidden inside the collapsed Advanced section on ha-mqtt-dmx and ha-mqtt-dmx-group — but Node-RED still shows a warning triangle on the node when it's unset (a known, unavoidable NR behaviour for config-node-reference fields, confirmed via live testing to be safe to ignore — the fixture/group works identically either way). Hiding the flagged field made it worse, not better: the warning was visible but the field causing it wasn't. Moved this field into the main visible form on both node types, and added a plain-language note on all 5 node types explaining the warning triangle is expected and doesn't affect deployment. Button, PIR, and Relay already had this field visible (no Advanced section on those three) — only DMX and Group needed the actual move; all 5 got the clarifying help text for consistency. |
| 0.9.18 | new: added a Node-RED Examples-library flow ("Quick Start — Site + Default Lists") — import via Node-RED's own menu → Import → Examples. Provides a placeholder MQTT broker, a linked Site node, and four pre-made Lists (L / P / BTN / PIR) so DMX/Relay/Button/PIR/Group nodes have a sensible list option in their dropdown from the very first fixture, instead of starting blank. Deliberately does not include a Zone — that step still needs the user's real broker/site details and is the one thing that can't be meaningfully pre-filled. This is a plain example flow using Node-RED's standard, documented mechanism (an `examples/` folder) — no custom install logic, no changes to node defaults, nothing that could regress in a future NR version the way the removed custom fixture-list UI did. `dev-tools/schedule-import/bootstrap_site.py` (the separate developer tool for generating a *specific* real site's config from spreadsheet data) is unrelated to this and was not changed. |
| 0.9.17 | fix: the "fixture list" / "PIR list" / "position list" config-node selector was showing an "invalid properties" error on every single node type, confirmed present on both Node-RED 4.1.10 and 5.0.0 — not a version-specific regression. Root cause: all 5 node types hid Node-RED's own native config-node UI (a dropdown of existing lists plus an automatic pencil-icon button to create or edit one) and replaced it with a hand-rolled equivalent that called NR's internal, undocumented `RED.editor.editConfig()` function directly. That internal call was crashing silently, meaning a new list could never actually get linked back to the node — leaving the field permanently blank and the node permanently invalid. Fix: removed the custom replacement UI entirely across all 5 node types (ha-mqtt-dmx, ha-mqtt-relay, ha-mqtt-button, ha-mqtt-pir, ha-mqtt-dmx-group) and restored Node-RED's own native, documented mechanism, which has worked correctly all along underneath our own code. No more calls to the internal editConfig function anywhere in the package. |
| 0.9.16 | fix: DMX lights and DMX groups were publishing `color_mode` in the MQTT state payload even when turning OFF, with no accompanying color data — Home Assistant correctly flagged this as an "invalid or incomplete color value" on every single OFF transition. Root cause was in the target adapters (`profiles/target/homeassistant.js` and `homeassistant-group.js`), not the calling nodes — `color_mode` is now omitted entirely from the OFF payload, matching what HA's schema actually expects. This was a live, currently-occurring bug (not historical), confirmed via real Docker rig logs; fix verified functionally, not just by reading the code — the affected function was called directly and its actual output payload checked before and after; fix: the "no list found — create one" Add button on ha-mqtt-pir and ha-mqtt-button pointed at the wrong field name (`node-input-fixtureList`, copy-pasted from the DMX/relay nodes) instead of each node's own field (`node-input-pirList` / `node-input-buttonList`) — creating a new list via that button never actually linked it back to the node, leaving the field unset. Both now correctly target their own field. |
| 0.9.15 | fix: ha-mqtt-relay's relayNum field was required unconditionally, even when DiscoveryMode is set to 'disabled' (not yet wired) — meaning a disabled placeholder relay could never validate cleanly without a real relay number assigned. Now conditionally required: only enforced when DiscoveryMode isn't 'disabled', matching how DMX channel fields already behave for disabled placeholder lights. Lets you pre-populate a schedule with disabled placeholders for fixtures you haven't wired yet, then assign real relay numbers and flip DiscoveryMode to 'enabled' once on site. |
| 0.9.14 | **BREAKING (internal only, no user-facing change):** all controller profile identifiers renamed to the "Pragiom" naming convention — pragiom_dmx_v1/v2 (was etherten_v1/v2), pragiom_btn_v1/v2 (was mw3d_v1/v2), pragiom_pir_v1/v2 (was mw3d_v1/v2), pragiom_relay_v1 (was bedrock_v1). Applied clean with no migration layer since no production deployments exist yet referencing the old identifiers — if you have saved flows referencing the old profile ids, they will need to be updated to the new ids before this version. Display labels and dropdown text are unaffected. |
| 0.9.13 | fix: Fixture List "+ Add" button was missing when zero 'lists' config node instances existed, leaving users with no way to create one from the node editor (all 5 node types); fix: 'Area 52' fallback sentinel was never reachable in practice because the resolved ha-mqtt-area config node (areaNode) was fetched but never read — now correctly prefers the real configured area name, falling back to the intentional 'Area 52' sentinel only when genuinely nothing else is available; fix: HA friendly names were doubling up (e.g. "Downlight in the Kitchen Downlight in the Kitchen") because entity name and device name were nearly identical strings with no has_entity_name flag — added has_entity_name to all 5 target adapters, single-entity node types now report name:null (device name alone), the two-entity button node keeps short distinguishing names ('Button' / 'UI Button') instead of full duplicated location text |
| 0.9.12 | fix: all 5 node types now set node.status() before returning on missing config/broker (previously errored silently with no status badge); fix: duplicated/malformed footer version string corrected across all 10 HTML files; content: Uplight added to L fixture list; content: Door Strike added to SubArea suggestions; internal: profile labels and comments genericised (no functional change) |
| 0.9.11 | Internal build — version number retired out of respect; superseded by 0.9.12 |
| 0.9.10 | patch: version bump (0.9.9 publish error) |
| 0.9.9 | ux: fixture list selector filters to matching prefix only (L/P/BTN/PIR per node type) |
| 0.9.8 | fix: lists.html oneditsave had duplicate const prefixSelect/prefixCustomInput declarations — killed script before registerType |
| 0.9.7 | fix: discovery mode tint no longer bleeds background into open dropdown — border+color only; window.X globals audit all HTML; lists.html footer added; README duplicate 0.8.1 entries removed |
| 0.9.6 | fix: lists node still not registering — wrapped lists.html JS in IIFE to prevent any global scope collision regardless of prior const poisoning |
| 0.9.5 | fix: lists node not registering in editor — FIXTURE_LIST_BY_PREFIX const redeclaration killed script before RED.nodes.registerType; all top-level HTML constants converted from const to var with guard (HA_DMX_AREAS, HA_DMX_SUB_AREAS, HA_AREA_SUGGESTIONS, HA_SUBAREA_SUGGESTIONS, FIXTURE_LIST_BY_PREFIX) |
| 0.9.4 | feat: wired command source — DMX and Relay nodes accept commands via NR wire (msg.topic filter on fixture ID); MQTT command subscription skipped when wired mode selected; normaliser handles NRHA entity ID format (light.l_970 → L-970); discovery and state reporting unchanged |
| 0.9.3 | fix: lists + button injected in all 5 node editors when NR omits it (zero instances); ui: Discovery Mode moved to top of all node editors with colour tint (green/amber/grey); live Fixture/Cable/Group ID preview updates as you type; emoji section headers with colour coding (🎛 Controller green, 🔧 Advanced purple); collapsible Advanced section in DMX and Group nodes; DMX channel labels dim when channel is unset; custom base64 icon support added (PIR icon shipped) |
| 0.9.1 | fix: ha-mqtt-site Target Profile dropdown widened; discovery prefix ghost text layout fixed; ha-mqtt-system duplicate footer removed, Apply buttons aligned, Config label renamed to Site; ha-mqtt-config orphaned Discovery prefix field and empty MQTT Broker header removed, Zone required indicator added |
| 0.9.0 | feat: engine/adapter split — canonical command/state objects; homeassistant.js and standalone.js adapters extracted from all 5 nodes; targetProfile selector on ha-mqtt-site (homeassistant / standalone); standalone/potato mode — no discovery, direct MQTT control without Home Assistant |
| 0.8.3 | fix: group node duplicate Fixture List removed; ha-mqtt-site discovery prefix auto-defaults to homeassistant; system node UI: ADD before REMOVE, Enable before Disable, Type on own line, footer added |
| 0.8.2 | BREAKING: new ha-mqtt-site config node — site-level config (broker, siteId, discoveryPrefix, QoS, retain); ha-mqtt-config now references ha-mqtt-site instead of owning broker/siteId; ha-mqtt-system now references ha-mqtt-site directly; config hierarchy: ha-mqtt-site → ha-mqtt-config (zone) → leaf nodes; migration: create ha-mqtt-site node, select it in each ha-mqtt-config |
| 0.8.1 | refactor: Node 24 / NR 5.0 modernisation — Set.difference() in lists merge, spread operator, rest params in buildTopic, arrow functions in dmx/group forEach/map/sort/filter, var→const in HTML oneditprepare blocks; README updated: nodes table, requirements (NR 5.0 / Node 24), ha-mqtt-system and lists documentation sections |
| 0.8.0 | feat: ha-mqtt-system node — single control surface replacing inject-node wall; discovery add/remove, debug enable/disable, runtime floor/rate/transition overrides, node disable/enable, status health check, persistent site-wide overrides; system control expansion in all 5 nodes (debug/floor/rateLimit/onTransition/offTransition/disable/enable/status); lists node: standard/custom/hidden entry model — standard list updates with npm, custom entries survive updates, useCustom toggle non-destructive, reset with confirmation |
| 0.7.9 | fix: group node const declaration conflict resolved; lists selectors moved to ADVANCED in all nodes; button/PIR cleaned up (no duplicate Fixture List); lists node Entries rename + prefix change updates list; ⚠ prefix on all warn messages; DMX field labels: ⬇ DMX ON Roof Value, ⬆ DMX ON Floor Value, γ Gamma Correction, ⤴ Brightness ON bump; README and help text updated with field descriptions |
| 0.7.8 | feat: new `lists` config node — generic configurable list for device types, button positions, PIR types and future use; Device Type autocomplete on DMX/Relay/Group; Button Position free-text + autocomplete; PIR Type free-text + autocomplete; floor __custom__ save bug fixed; Site ID inline autocomplete; L/P/F prefix naming convention; single-space startup log |
| 0.7.7 | fix: Zone and Floor required fields on config node; Site ID autocomplete from existing config nodes; Sub-Area select reordered (none first, then $(SUB_AREA), then area subs); sub-area now saveable as empty; startup log location single-spaced |
| 0.7.6 | fix: Zone field on config node changed from hardcoded select back to free-text input — allows adding new zones |
| 0.7.5 | fix: startup log format — node type (DMX/Relay/Button/PIR) declared before dash, device type after; consistent across all 5 nodes |
| 0.7.4 | feat: enriched startup log — all 5 nodes now log fixture type, zone/area/sub-area location, and node-specific details (DMX channels+colorMode, relay topic+payload, button topic+payload, PIR topic+payload, group name); unset fields skipped |
| 0.7.3 | fix: Sub-Area changed to proper select dropdown populated from ha-mqtt-area config; Config label renamed to Zone — all 5 nodes |
| 0.7.2 | fix: sub-area dropdown now populates after returning from ha-mqtt-area config editor (setTimeout re-run of _updateSubAreaList on oneditprepare — all 5 nodes) |
| 0.7.1 | fix: areaConfig exposed as proper config node selector (✏️) in all 5 nodes — fixes 570 invalid nodes on import; engines bumped node >=24.0.0, node-red >=5.0.0 |
| 0.7.0 | NEW: ha-mqtt-area config node — area registry with filtered sub-areas, global scope, backwards compatible |
| 0.6.68 | UI polish: around to situation, discovery prefix red warning, DMX floor help, relay MQTT segment warning, PIR/button placeholder fix |
| 0.6.67 | 5-level hierarchy: Floor field added to config node, all lists updated (Area/SubArea/Situation/DeviceType), Option A/B UI, canvas labels include floor |
| 0.6.66 | Fix canvas label env var walker — RED.nodes.group() not RED.nodes.node() for groups |
| 0.6.65 | Canvas labels walk NR group hierarchy to resolve env vars correctly |
| 0.6.64 | Fix canvas label crash — try/catch around RED.util.evaluateEnvProperty in label function |
| 0.6.63 | Canvas labels resolve $(VAR) group env vars via RED.util.evaluateEnvProperty. $(SUB_AREA) added to DMX and group subLocation dropdown |
| 0.6.62 | Help text added explaining $(VAR) group env var syntax in all nodes |
| 0.6.61 | $(AREA), $(SITUATION), $(SUB_AREA) as default dropdown options — NR group env var inheritance via native $() syntax |
| 0.6.60 | Removed _env() group env var inheritance — use NR native $(VAR) syntax in node fields instead |
| 0.6.59 | Situation dropdown: blank option added, default changed to blank, fallback to "in" at runtime |
| 0.6.58 | Area dropdown: blank option added as first item, default changed to blank so group env var inheritance works out of the box |
| 0.6.57 | Area fallback defaults to Area 52 (TBC) when blank and no group env var set |
| 0.6.56 | Safe _env() helper fix — correctly packaged |
| 0.6.55 | Fix env var helper — safe _env() wrapper replaces node.env.get() for NR version compatibility |
| 0.6.54 | Fix group env var inheritance — correct API node.env.get() replaces RED.util.evaluateEnvProperty() which is not available in node runtime |
| 0.6.53 | Group env var inheritance — area/situation/subLocation/deviceType now inherit from NR group env vars (AREA, SITUATION, SUB_AREA, DEVICE_TYPE) if node field left blank. Situation: on/near added all nodes. |
| 0.6.52 | Critical fix: stray </script> tag in ha-mqtt-dmx.html caused node to disappear from palette. Ceiling added to Area list all nodes. Bridge corruption fixed. |
| 0.6.51 | MEMBERS header green, member input wider, Name placeholder improved all 5 nodes, hr shadow lines removed, Bridge added to Sub-Area all nodes, member description fix on dialog reopen |
| 0.6.50 | Broker crash fix — empty subscribe topic guard in button and PIR nodes. Autocomplete: description updates immediately on selection, input blurs after select. Groups added to member autocomplete |
| 0.6.49 | subLocation added to canvas label in relay, button and PIR nodes |
| 0.6.48 | Duplicate members now skipped with warning instead of blocking entire group command. Autocomplete dropdown wider, text wraps instead of truncating |
| 0.6.47 | Default deviceType changed to Light all nodes. Canvas label includes zone + subLocation all 5 nodes. Section headers brighter with separator line. Rate limit max raised to 10 |
| 0.6.46 | Member list rows show fixture description alongside ID as dim read-only label, updates live as you type |
| 0.6.45 | Member list autocomplete: description updates immediately on selection, input blurs. Groups added to ALLOWED_TYPES in registry endpoint |
| 0.6.44 | Canvas label zone fix all 5 nodes. Section headers #7eb8d4 with separator line. Relay situation options aligned with DMX. Group ID row widened |
| 0.6.43 | Canvas label zone fix all 5 nodes, deviceType empty-string guard (dmx/relay/group), Group ID row widened, postfix options -ALL/-RELAY/-Power added, relay situation options aligned with DMX |
| 0.6.42 | Member list custom inline autocomplete dropdown — shows fixture ID + friendly name, filters by ID or description, DMX + relay only. Label stored in fixture registry on startup |
| 0.6.41 | Registry endpoint fallback — scans all ha-mqtt-config nodes if siteId not resolved |
| 0.6.40 | Member list autocomplete first attempt (datalist — superseded by 0.6.42) |
| 0.6.39 | README version history updated |
| 0.6.38 | etherten_v2 set as default controller profile — nodes with unset profile now correctly use batched JSON array format |
| 0.6.37 | ha-mqtt-dmx-group palette registration fixed — duplicate `const node = this` in oneditprepare caused silent browser error preventing node appearing in palette |
| 0.6.36 | README settings.js path corrected to /addon_configs/a0d7b954_nodered/settings.js |
| 0.6.35 | pubState OFF fix — recovery and restoreAfterEffect no longer send colour values when state is OFF |
| 0.6.34 | broker.setMaxListeners(0) added to all 5 nodes — suppresses MaxListeners warning |
| 0.6.33 | transitionRateLimit exposed in config node UI (was hardcoded) |
| 0.6.32 | README full rewrite |
| 0.6.31 | Group Node v2 — member list (local + remote), internal routing via RED.nodes.getNode(), duplicate detection, loop protection cross-instance. Registry stores nodeId + nodeType. |
| 0.6.30 | Debug timer leak fixed — S._debugTimer saved and cleared on node close (all 5 nodes) |
| 0.6.29 | Node label updated to match HA device format including subLocation |
| 0.6.28 | Per-node DMX floor override field — blank inherits config default |
| 0.6.27 | DMX floor enforced at sendDmxChannels level — absolute gate, covers all code paths |
| 0.6.26 | pubState recovery + restoreAfterEffect floor fix (reverted — wrong approach) |
| 0.6.25 | pubState handleON floor fix (reverted — wrong approach) |
| 0.6.24 | User published manually |
| 0.6.23 | colorValue>=3 bleed guard, ON/OFF transition UI fields (0-60s), brightness=0 hard zero restored |
| 0.6.22 | Transition floor in runTransition, default OFF transition config field |
| 0.6.21 | Reverted bad rgbw_color parsing block |
| 0.6.20 | colorValue>=3 guard added, ON transition UI field |
| 0.6.19 | unregisterFixtureId() on close (all 5 nodes), setMaxListeners(0), brightness=0 hard zero |
| 0.6.17 | Group node separated from DMX type, README updated |
| 0.4.4 | Recovery status shows actual state on canvas |
| 0.4.3 | device:remove no longer clears disk state |
| 0.4.2 | Group node pubState includes color |
| 0.4.1 | Group node status reflects ON/OFF correctly |
| 0.4.0 | Group recovery no longer forwards state to children |
| 0.1.0 | Initial release |

---

## Author

DeSwaggy — Discord: @deswaggy

## Licence

This package is licensed under **[GPL v3](https://www.gnu.org/licenses/gpl-3.0.txt)**.

You are free to use, modify, and distribute this package. Any derivative works must be distributed under the same GPL v3 licence. This package may not be taken proprietary, rebranded as closed source, or distributed without crediting the original author.

This package was built from 6+ years of real-world professional installation experience. If you use it, improve it, or build on it — please contribute your changes back to the community.

---

> **Note:** This is building automation DMX — fixtures, decoders, dimmers, relay switching. Not entertainment industry DMX (no movers, gobos, or fixture profiles). DMX is an open standard and this package works with any DMX controller that accepts MQTT payloads.
