node-red-contrib-dmx-for-ha 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,278 @@
1
+ # Node Input / Output Contracts
2
+ **Status:** Pre-packaging specification
3
+ **Purpose:** Blueprint for .html editor files and node .js runtime code
4
+ **Principle:** Simple by default. Zero outputs except where structurally required.
5
+
6
+ ---
7
+
8
+ ## Package Name (proposed)
9
+ `node-red-contrib-dmx-for-ha`
10
+
11
+ ---
12
+
13
+ ## Shared Input Contract (all nodes)
14
+
15
+ Every node accepts the following on its single input port:
16
+
17
+ | Message type | Shape | Source | Action |
18
+ |---|---|---|---|
19
+ | Device add | `msg.device = "add"` | SYSTEM node inject | Run MQTT discovery, subscribe to HA cmd topic |
20
+ | Device remove | `msg.device = "remove"` | SYSTEM node inject | Clear HA entities, unsubscribe, clear memory |
21
+ | HA command | `msg.payload.state = "ON"/"OFF"` | MQTT In (internal) | Process state change |
22
+
23
+ The MQTT broker connection, subscribe loop, and all publish operations are
24
+ handled **internally** — no external MQTT In or MQTT Out nodes required.
25
+
26
+ ---
27
+
28
+ ## 1. DMX Group Node (v0.3.8)
29
+
30
+ ### Purpose
31
+ Virtual fixture group. Receives a single HA command and fans it out to all
32
+ downstream nodes. No physical hardware — exists only in Node-RED.
33
+
34
+ ### Input port (×1)
35
+ | Message | Shape | Source |
36
+ |---|---|---|
37
+ | Device add/remove | `msg.device = "add"/"remove"` | SYSTEM node |
38
+ | HA command | `msg.payload.state`, optional `brightness`, `color`, `transition`, `effect` | Internal MQTT (HA dashboard) |
39
+ | Cascade (upstream) | `msg.dmx_trace = {source, path, depth}` + `msg.payload` | Upstream Group Node cascade output |
40
+
41
+ ### Output port (×1) — Link
42
+ Fans the HA command payload downstream to child nodes.
43
+ Wire to: DMX Node input, Relay Node input, or another Group Node input.
44
+
45
+ Output message shape:
46
+ ```javascript
47
+ {
48
+ dmx_trace: {
49
+ source: "LG-992", // this group's ID
50
+ path: ["LG-992"], // breadcrumb trail for loop detection
51
+ depth: 1 // increments at each Group Node hop
52
+ },
53
+ payload: { // original HA command, passed through unchanged
54
+ state: "ON",
55
+ brightness: 255,
56
+ color: { r:255, g:128, b:0, w:255 },
57
+ transition: 1.5,
58
+ effect: "rainbow" // if applicable
59
+ }
60
+ }
61
+ ```
62
+
63
+ ### Internal MQTT behaviour
64
+ - Subscribes to: `homeassistant/light/LG-{id}/cmd`
65
+ - Publishes discovery to: `homeassistant/light/LG-{id}/config`
66
+ - Publishes state to: `homeassistant/light/LG-{id}/state`
67
+
68
+ ### Loop detection
69
+ If `msg.dmx_trace.path` already contains this node's ID, the message is
70
+ dropped and a `node.warn` fires. Prevents infinite loops in circular wiring.
71
+
72
+ ---
73
+
74
+ ## 2. DMX Node (v0.5.9)
75
+
76
+ ### Purpose
77
+ Terminal fixture node. Receives commands from HA or a Group Node cascade,
78
+ translates to DMX channel values and publishes via MQTT to the DMX controller.
79
+
80
+ ### Input port (×1)
81
+ | Message | Shape | Source |
82
+ |---|---|---|
83
+ | Device add/remove | `msg.device = "add"/"remove"` | SYSTEM node |
84
+ | HA command | `msg.payload.state`, optional `brightness`, `color`, `transition`, `effect` | Internal MQTT (HA dashboard) |
85
+ | Cascade | `msg.dmx_trace` + `msg.payload` | Group Node cascade output |
86
+
87
+ ### Output ports
88
+ **None.** Terminal node — DMX is the end of the signal chain.
89
+
90
+ ### Internal MQTT behaviour
91
+ - Subscribes to: `homeassistant/light/{prefix}-{id}{postfix}/cmd`
92
+ - Publishes discovery to: `homeassistant/light/{prefix}-{id}{postfix}/config`
93
+ - Publishes state to: `homeassistant/light/{prefix}-{id}{postfix}/state`
94
+ - Publishes DMX to: `MW3D/{zone}/dmx/{universe}`
95
+ payload format: `"{channel3digits}{value3digits}"` e.g. `"212255"`
96
+
97
+ ### Supported colour modes
98
+ `rgbw` `rgbww` `rgb` `cct` `brightness` `onoff`
99
+
100
+ ---
101
+
102
+ ## 3. Relay Node (v4.0.2)
103
+
104
+ ### Purpose
105
+ Terminal 230V relay node. Receives ON/OFF commands from HA or a Group Node
106
+ cascade, publishes integer relay commands via MQTT to the relay controller.
107
+
108
+ **Not related to DMX.** Entirely separate control path and hardware.
109
+
110
+ ### Input port (×1)
111
+ | Message | Shape | Source |
112
+ |---|---|---|
113
+ | Device add/remove | `msg.device = "add"/"remove"` | SYSTEM node |
114
+ | HA command | `msg.payload.state = "ON"/"OFF"`, optional `effect` | Internal MQTT (HA dashboard) |
115
+ | Cascade | `msg.dmx_trace` + `msg.payload` | Group Node cascade output |
116
+
117
+ ### Output ports
118
+ **None.** Terminal node.
119
+
120
+ ### Internal MQTT behaviour
121
+ - Subscribes to: `homeassistant/light/{prefix}-{id}{postfix}/cmd`
122
+ - Publishes discovery to: `homeassistant/light/{prefix}-{id}{postfix}/config`
123
+ - Publishes state to: `homeassistant/light/{prefix}-{id}{postfix}/state`
124
+ - Publishes relay command to: `MW3D/{zone}/relay/{controller}/{relayNumber}`
125
+ payload: integer `1` (ON) or `0` (OFF)
126
+
127
+ ---
128
+
129
+ ## 4. Button Node (v5.0.3)
130
+
131
+ ### Purpose
132
+ Wall button receiver. Listens for hardware controller MQTT payloads and
133
+ publishes to HA as a `binary_sensor` (for automations) and a `button`
134
+ entity (for dashboard UI mirror). HA automation logic handles everything
135
+ downstream — no NR output required.
136
+
137
+ ### Input port (×1)
138
+ | Message | Shape | Source |
139
+ |---|---|---|
140
+ | Device add/remove | `msg.device = "add"/"remove"` | SYSTEM node |
141
+ | Physical press | `msg.payload = "{panelId}-{GPIOpin}"` plain string | Hardware controller via internal MQTT |
142
+ | UI press | `msg.topic = HA button cmd topic` + `msg.payload = "PRESS"` | HA dashboard via internal MQTT |
143
+
144
+ ### Output ports
145
+ **None.** HA receives the event via `binary_sensor` state topic.
146
+ Automation logic lives in HA — not in Node-RED.
147
+
148
+ ### Internal MQTT behaviour
149
+ - Subscribes to: `{controllerTopic}` (e.g. `buttons`) — plain string payloads
150
+ - Subscribes to: `homeassistant/button/{prefix}-{id}{postfix}-BTN/cmd`
151
+ - Publishes binary_sensor discovery to: `homeassistant/binary_sensor/{prefix}-{id}{postfix}/config`
152
+ - Publishes button discovery to: `homeassistant/button/{prefix}-{id}{postfix}-BTN/config`
153
+ - Publishes binary_sensor state to: `homeassistant/binary_sensor/{prefix}-{id}{postfix}/state`
154
+ payload: `"ON"` on press, HA `off_delay` handles auto-clear
155
+
156
+ ### Dual entity design
157
+ Two HA entities per button:
158
+ - `binary_sensor.s_10_a` — automation trigger, state changes ON then auto-clears
159
+ - `button.s_10_a_btn` — dashboard UI mirror, press from HA frontend triggers same code path
160
+
161
+ ---
162
+
163
+ ## 5. PIR Node (v1.0.3)
164
+
165
+ ### Purpose
166
+ PIR / motion sensor receiver. Listens for hardware controller MQTT payloads
167
+ and publishes to HA as a `binary_sensor` with `device_class: motion`.
168
+ Includes configurable warm-up delay on boot to prevent false automation
169
+ triggers during system startup.
170
+
171
+ ### Input port (×1)
172
+ | Message | Shape | Source |
173
+ |---|---|---|
174
+ | Device add | `msg.device = "add"` | SYSTEM node |
175
+ | Device remove | `msg.device = "remove"` | SYSTEM node |
176
+ | Availability toggle | `msg.device = "avty"` + `msg.payload = "online"/"offline"` | SYSTEM node or admin |
177
+ | Motion detected | `msg.payload = "{panelId}-{GPIOpin}"` plain string | Hardware controller via internal MQTT |
178
+
179
+ ### Output ports
180
+ **None.** HA receives motion events via `binary_sensor` state topic.
181
+ Automation logic lives in HA — not in Node-RED.
182
+
183
+ ### Internal MQTT behaviour
184
+ - Subscribes to: `{controllerTopic}` (e.g. `MW3D/Master/PIR_Sensors`) — plain string
185
+ - Publishes discovery to: `homeassistant/binary_sensor/{prefix}-{id}{postfix}/config`
186
+ - Publishes state to: `homeassistant/binary_sensor/{prefix}-{id}{postfix}/state`
187
+ payload: `"ON"` on motion, HA `off_delay` handles auto-clear
188
+ - Publishes availability to: `homeassistant/binary_sensor/{prefix}-{id}{postfix}/avty`
189
+ payload: `"online"` / `"offline"`
190
+
191
+ ### Warm-up sequence
192
+ On `device:add`:
193
+ 1. Immediately publishes `offline` to availability topic
194
+ 2. Starts configurable warm-up timer (default 120s)
195
+ 3. After timer: publishes `online` — PIR is now active
196
+ 4. Prevents false motion triggers during NR startup cascade
197
+
198
+ ---
199
+
200
+ ## Output Label — Group Node Cascade
201
+
202
+ The single output on the Group Node needs a label that is:
203
+ - Self-explanatory on first read
204
+ - Not too technical
205
+ - Accurate to what gets wired to it
206
+
207
+ **Candidates:**
208
+ | Label | Notes |
209
+ |---|---|
210
+ | `Cascade` | Accurate but slightly technical |
211
+ | `Downstream` | Clear directional meaning |
212
+ | `Link` | Simple, generic |
213
+ | `To fixtures` | Descriptive but assumes only fixtures downstream |
214
+ | `Output` | Generic but honest |
215
+
216
+ **Output label: `Link`** ✓ Locked in.
217
+
218
+ ---
219
+
220
+ ## Canvas Topology (reference)
221
+
222
+ ```
223
+ SYSTEM node
224
+ │ msg.device = "add"
225
+ ├──────────────────────────────────────────────────────┐
226
+ │ │
227
+ ▼ ▼
228
+ [DMX Group Node LG-992] [Button Node S-10-A]
229
+ │ cascade output (Link) (no output — HA handles automation)
230
+ ├──────────────┬──────────────┐
231
+ ▼ ▼ ▼
232
+ [DMX Node [DMX Node [Relay Node
233
+ L-992-A] L-992-B] P-51]
234
+ (no output) (no output) (no output)
235
+
236
+ ▼ ▼ ▼
237
+ MQTT MQTT MQTT
238
+ DMX ctrl DMX ctrl Relay ctrl
239
+
240
+ └──────────────┴──────────────┘
241
+
242
+
243
+ MQTT Broker
244
+
245
+
246
+ Home Assistant
247
+ (all automation logic)
248
+ ```
249
+
250
+ ---
251
+
252
+ ## Pre-packaging Checklist
253
+
254
+ - [ ] Agree on `Link` vs alternative for cascade output label
255
+ - [x] Package name: `node-red-contrib-dmx-for-ha`
256
+ - [x] Palette category: `DMX for HA`
257
+ - [x] Display names: DMX, DMX Group, Relay, Button, PIR
258
+ - [x] Internal types: ha-mqtt-dmx, ha-mqtt-dmx-group, ha-mqtt-relay, ha-mqtt-button, ha-mqtt-pir
259
+ - [x] Cascade output label: `Link`
260
+ - [x] Zero outputs on DMX, Relay, Button, PIR
261
+ - [x] Passthrough output removed from all node code
262
+ - [x] node.warn messages updated — no passthrough references
263
+ - [x] Shared utility module decision: INLINE for v1, extract in v2
264
+ - [x] MQTT broker via config node (Option 3)
265
+ - [x] Config node: `ha-mqtt-config` — one per zone
266
+ - [x] Zone in config node — multi-zone = multiple config nodes
267
+ - [x] MQTT settings in config node, topics per node
268
+ - [x] Relay controller MQTT: broker in config, topic built per node
269
+ - [ ] Write ha-mqtt-config .html + .js (FIRST — all other nodes depend on it)
270
+ - [ ] Write ha-mqtt-dmx .html + .js
271
+ - [ ] Write ha-mqtt-dmx-group .html + .js
272
+ - [ ] Write ha-mqtt-relay .html + .js
273
+ - [ ] Write ha-mqtt-button .html + .js
274
+ - [ ] Write ha-mqtt-pir .html + .js
275
+ - [ ] Write package.json
276
+ - [ ] Test packaged install end to end
277
+ - [ ] Test all nodes reference config node correctly
278
+ - [ ] Publish to flows.nodered.org
@@ -0,0 +1,258 @@
1
+ # Node-RED 4.1.x — Subflow Development Gotchas
2
+
3
+ A collection of non-obvious issues discovered while building a production subflow
4
+ library for a Home Assistant / MQTT lighting control system on Node-RED 4.1.6.
5
+ Posting these here so others don't spend hours rediscovering them.
6
+
7
+ ---
8
+
9
+ ## 1. You Cannot Import a Subflow Definition and Its Instances in the Same JSON
10
+
11
+ This is the big one. If you export a flow that contains both a subflow definition
12
+ and instances of that subflow on a tab, and try to re-import it — NR will throw:
13
+
14
+ ```
15
+ TypeError: Cannot read properties of undefined (reading 'length')
16
+ ```
17
+
18
+ There is no helpful error message. It fails silently in a confusing way.
19
+
20
+ ### Why It Happens
21
+
22
+ When NR processes a JSON import, it encounters the subflow instance
23
+ (`"type": "subflow:abc123"`) and tries to look up the subflow definition to
24
+ validate the port count. If the definition hasn't been fully registered yet
25
+ (even if it appears earlier in the same JSON array), the lookup fails.
26
+
27
+ ### The Fix — Two-Step Import
28
+
29
+ Split your flow into two separate JSON files:
30
+
31
+ **File 1 — Subflow definitions only** (no tab, no instances):
32
+ ```json
33
+ [
34
+ { "id": "abc123", "type": "subflow", "name": "My Node", ... },
35
+ { "id": "fn001", "type": "function", "z": "abc123", ... },
36
+ { "id": "mq001", "type": "mqtt out", "z": "abc123", ... }
37
+ ]
38
+ ```
39
+
40
+ **File 2 — Tab and instances only** (no subflow definitions):
41
+ ```json
42
+ [
43
+ { "id": "tab001", "type": "tab", ... },
44
+ { "id": "inst01", "type": "subflow:abc123", "z": "tab001", ... }
45
+ ]
46
+ ```
47
+
48
+ **Import sequence:**
49
+ 1. Import File 1 — **do NOT deploy yet**
50
+ 2. Import File 2 immediately (while File 1 is still in memory)
51
+ 3. Deploy both together
52
+
53
+ The key is step 2 must happen before deploying. Once you deploy File 1, NR
54
+ registers the subflow definitions. You can then import File 2 separately and
55
+ deploy again. Both sequences work — the important thing is the definition must
56
+ be in NR's memory before the instance import is processed.
57
+
58
+ ### Bonus Gotcha — ID Conflicts
59
+
60
+ If you re-import subflow definitions that already exist in NR (same IDs), NR
61
+ will show a conflict warning and offer Import vs Copy. **Copy assigns new IDs**
62
+ which then breaks File 2 (the instances still reference the old IDs).
63
+
64
+ Solution: bump the version and regenerate all node IDs each time you publish
65
+ a new version of your subflow definitions. Every node in the file — subflow
66
+ definition, internal function nodes, internal MQTT nodes, link nodes — needs
67
+ a fresh ID. NR generates IDs as 16-character hex strings (`secrets.token_hex(8)`
68
+ in Python).
69
+
70
+ ---
71
+
72
+ ## 2. The `tostatus: true` Debug Node Does Not Work Inside Subflows
73
+
74
+ If you place a debug node inside a subflow with `tostatus: true` (the "Show
75
+ as status" option), the status text will **not** appear under the subflow
76
+ instance node on the canvas. It works fine for regular function nodes but
77
+ does not propagate through the subflow boundary.
78
+
79
+ ### The Correct Solution — Subflow Status Port
80
+
81
+ NR has a built-in mechanism for this that is easy to miss:
82
+
83
+ 1. Open your subflow definition (double-click the subflow in the palette)
84
+ 2. Click the **edit properties** button (pencil icon, top left)
85
+ 3. Tick the **"Status"** checkbox — this adds a special status input port
86
+
87
+ In the subflow JSON this appears as a `status` field on the subflow definition:
88
+
89
+ ```json
90
+ {
91
+ "id": "abc123",
92
+ "type": "subflow",
93
+ "name": "My Node",
94
+ "status": {
95
+ "x": 555,
96
+ "y": 195,
97
+ "wires": [{ "id": "fn001", "port": 2 }]
98
+ }
99
+ }
100
+ ```
101
+
102
+ The `port` value is **zero-indexed**, so `"port": 2` = output 3 of the function
103
+ node. Wire your function node's status output to this port and status messages
104
+ will correctly appear under the subflow instance on the canvas.
105
+
106
+ ### What the Function Node Should Send
107
+
108
+ The status port expects the standard NR status object on `msg.status`:
109
+
110
+ ```javascript
111
+ node.send([
112
+ null, // output 1
113
+ null, // output 2
114
+ { // output 3 → status port
115
+ status: {
116
+ fill: "green",
117
+ shape: "ring",
118
+ text: "L-991 ready — awaiting HA commands"
119
+ }
120
+ },
121
+ null, // output 4
122
+ ]);
123
+ ```
124
+
125
+ Valid `fill` values: `"red"`, `"green"`, `"yellow"`, `"blue"`, `"grey"`
126
+ Valid `shape` values: `"ring"`, `"dot"`
127
+
128
+ ---
129
+
130
+ ## 3. Subflow Env Var JSON — All Values Must Be Strings
131
+
132
+ When hand-crafting subflow env var definitions in JSON, all `value` fields must
133
+ be strings — even for `num` and `bool` types. NR will silently fail or behave
134
+ unexpectedly if you pass integers or booleans.
135
+
136
+ **Wrong:**
137
+ ```json
138
+ { "name": "<HA_MQTT_QOS>", "type": "num", "value": 0 }
139
+ { "name": "<HA_enabled>", "type": "bool", "value": true }
140
+ ```
141
+
142
+ **Correct:**
143
+ ```json
144
+ { "name": "<HA_MQTT_QOS>", "type": "num", "value": "0" }
145
+ { "name": "<HA_enabled>", "type": "bool", "value": "true" }
146
+ ```
147
+
148
+ ---
149
+
150
+ ## 4. Font Awesome Icons on Env Var Labels
151
+
152
+ You can add icons to env var labels in the subflow properties panel — they
153
+ appear next to the label text and make the panel much easier to scan. Add an
154
+ `icon` field to the `ui` object:
155
+
156
+ ```json
157
+ {
158
+ "name": "<DEVICE_area>",
159
+ "type": "str",
160
+ "value": "",
161
+ "ui": {
162
+ "label": { "en-US": "Area:" },
163
+ "type": "select",
164
+ "icon": "font-awesome/fa-map-marker",
165
+ "opts": { "opts": [ ... ] }
166
+ }
167
+ }
168
+ ```
169
+
170
+ Useful icon mappings for home automation subflows:
171
+
172
+ | Field type | Icon |
173
+ |---|---|
174
+ | Zone / Location | `font-awesome/fa-location-arrow` |
175
+ | Area | `font-awesome/fa-map-marker` |
176
+ | Sub-area | `font-awesome/fa-map-marker` |
177
+ | Device type | `font-awesome/fa-lightbulb-o` |
178
+ | Colour mode | `font-awesome/fa-sliders` |
179
+ | ID / Prefix / Postfix | `font-awesome/fa-tv` |
180
+ | Channel numbers | `font-awesome/fa-sort-numeric-asc` |
181
+ | Timing fields | `font-awesome/fa-clock-o` |
182
+ | MQTT topics | `font-awesome/fa-exchange` |
183
+ | HA Icon field | `font-awesome/fa-image` |
184
+ | Section headers | `font-awesome/fa-hand-spock-o` |
185
+
186
+ Section separator entries (type `none`) also support icons and are useful for
187
+ visually grouping related env vars:
188
+
189
+ ```json
190
+ {
191
+ "name": "SETTINGS_Required",
192
+ "type": "str",
193
+ "value": "",
194
+ "ui": {
195
+ "label": { "en-US": "SETTINGS (*Required)" },
196
+ "type": "none",
197
+ "icon": "font-awesome/fa-hand-spock-o"
198
+ }
199
+ }
200
+ ```
201
+
202
+ ---
203
+
204
+ ## 5. MQTT In Node Inside a Subflow — Plain String Payloads
205
+
206
+ If your subflow's internal MQTT In node receives plain string payloads (e.g.
207
+ from a hardware controller publishing `"10-54"` rather than a JSON object),
208
+ set `datatype` to `"auto-detect"` not `"json"`.
209
+
210
+ With `datatype: "json"`, NR will throw:
211
+ ```
212
+ Failed to parse JSON string
213
+ ```
214
+
215
+ For nodes that receive HA commands (which are always JSON), `"json"` is correct.
216
+ For nodes that receive raw hardware controller payloads, use `"auto-detect"`.
217
+
218
+ ```json
219
+ {
220
+ "type": "mqtt in",
221
+ "datatype": "auto-detect"
222
+ }
223
+ ```
224
+
225
+ ---
226
+
227
+ ## 6. The MQTT In Node Can Subscribe to Multiple Topics
228
+
229
+ An MQTT In node with `inputs: 1` can receive subscribe/unsubscribe commands
230
+ from a function node. This lets one MQTT In node listen on multiple topics
231
+ simultaneously — useful when a node needs to receive both hardware controller
232
+ payloads AND HA dashboard command payloads.
233
+
234
+ Pattern:
235
+ ```javascript
236
+ // On device:add, send two subscribe messages on output 2
237
+ node.send([discoveryPayload, { topic: "hardware/topic", action: "subscribe" }, ...]);
238
+ node.send([uiDiscovery, { topic: "ha/cmd/topic", action: "subscribe" }, ...]);
239
+
240
+ // Entry point — differentiate by topic
241
+ if (msg.topic === "hardware/topic" && msg.payload === CFG.devicePayload) {
242
+ handlePhysicalPress();
243
+ }
244
+ if (msg.topic === "ha/cmd/topic" && msg.payload === "PRESS") {
245
+ handleUiPress();
246
+ }
247
+ ```
248
+
249
+ The same MQTT In node serves as receiver for both, and both routes execute
250
+ identical code paths.
251
+
252
+ ---
253
+
254
+ *Discovered during development of a Node-RED → Home Assistant DMX/relay/sensor
255
+ lighting control system. NR version 4.1.6, HA 2026.x.*
256
+
257
+ *If this saved you time, consider posting your own discoveries — future
258
+ developers (and future AI models) will thank you.*