node-red-contrib-dmx-for-ha 0.6.28 → 0.6.33
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.
- package/README.md +157 -329
- package/nodes/ha-mqtt-button.html +1 -1
- package/nodes/ha-mqtt-button.js +11 -5
- package/nodes/ha-mqtt-config.html +12 -1
- package/nodes/ha-mqtt-dmx-group.html +107 -19
- package/nodes/ha-mqtt-dmx-group.js +113 -6
- package/nodes/ha-mqtt-dmx.html +8 -6
- package/nodes/ha-mqtt-dmx.js +11 -5
- package/nodes/ha-mqtt-pir.html +1 -1
- package/nodes/ha-mqtt-pir.js +11 -5
- package/nodes/ha-mqtt-relay.html +1 -1
- package/nodes/ha-mqtt-relay.js +11 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -32,7 +32,7 @@ Node-RED acts as the **virtual lighting desk** — it owns the DMX channels, man
|
|
|
32
32
|
```
|
|
33
33
|
Home Assistant Node-RED DMX Hardware
|
|
34
34
|
────────────── ──────── ────────────
|
|
35
|
-
Automations ←──→ ha-mqtt-dmx ──→ EtherTen /
|
|
35
|
+
Automations ←──→ ha-mqtt-dmx ──→ EtherTen /
|
|
36
36
|
Dashboards ha-mqtt-group DMX decoder /
|
|
37
37
|
Scenes ha-mqtt-relay MQTT bridge
|
|
38
38
|
Voice control ha-mqtt-button ←── Wall buttons
|
|
@@ -55,27 +55,17 @@ Most DMX implementations require either expensive proprietary hardware or comple
|
|
|
55
55
|
|
|
56
56
|
- Full Home Assistant MQTT discovery — entities appear automatically in HA
|
|
57
57
|
- RGBW, RGBWW, RGB, Colour Temperature, Brightness, and On/Off colour modes
|
|
58
|
-
- Gamma-corrected DMX output
|
|
59
|
-
-
|
|
58
|
+
- Gamma-corrected DMX output with configurable floor and limiter
|
|
59
|
+
- Smooth transitions with configurable ON and OFF fade times
|
|
60
|
+
- Effects engine — strobe, rainbow, fire, flicker, twinkle, police and more
|
|
61
|
+
- Group control — virtual group entities fan commands to member fixtures
|
|
60
62
|
- State persistence across reboots — survives power cycles and HA updates
|
|
61
63
|
- Wall button and PIR motion sensor integration with remote commissioning support
|
|
62
64
|
- 230V relay switching
|
|
63
65
|
- DMX channel conflict detection — warns on duplicate channel assignments
|
|
64
|
-
-
|
|
66
|
+
- Per-node DMX floor — prevents low-value flicker on hardware that needs it
|
|
65
67
|
- Debug mode per node — 12hr auto-disable, safe to leave in production flows
|
|
66
68
|
|
|
67
|
-
## Licence
|
|
68
|
-
|
|
69
|
-
This package is licensed under **[GPL v3](https://www.gnu.org/licenses/gpl-3.0.txt)**.
|
|
70
|
-
|
|
71
|
-
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.
|
|
72
|
-
|
|
73
|
-
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.
|
|
74
|
-
|
|
75
|
-
---
|
|
76
|
-
|
|
77
|
-
> **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.
|
|
78
|
-
|
|
79
69
|
---
|
|
80
70
|
|
|
81
71
|
## Nodes included
|
|
@@ -84,7 +74,7 @@ This package was built from 6+ years of real-world professional installation exp
|
|
|
84
74
|
|---|---|---|---|
|
|
85
75
|
| `ha-mqtt-config` | *(config)* | — | Shared site config — broker, zone, HA settings |
|
|
86
76
|
| `ha-mqtt-dmx` | DMX | Yellow | Single DMX fixture |
|
|
87
|
-
| `ha-mqtt-dmx-group` | DMX Group | Gold | Virtual group — fans commands to
|
|
77
|
+
| `ha-mqtt-dmx-group` | DMX Group | Gold | Virtual group — fans commands to member fixtures |
|
|
88
78
|
| `ha-mqtt-relay` | Relay | Red | 230V relay switching |
|
|
89
79
|
| `ha-mqtt-button` | Button | Green | Wall button receiver |
|
|
90
80
|
| `ha-mqtt-pir` | PIR | Purple | Motion sensor receiver |
|
|
@@ -160,10 +150,13 @@ Context store : 'disk_values' [module=localfilesystem]
|
|
|
160
150
|
1. Drag a **DMX** node onto the canvas and open its editor
|
|
161
151
|
2. Click **Add new ha-mqtt-config...** next to the Config field
|
|
162
152
|
3. Fill in:
|
|
163
|
-
- **Config Label** — a friendly name e.g. "My House"
|
|
153
|
+
- **Config Label** — a friendly name e.g. "My House"
|
|
164
154
|
- **Site ID** — short slug for MQTT topics e.g. `home` (no spaces)
|
|
165
|
-
- **Zone** — physical zone e.g. `Master`, `BnB`. Used in MQTT topics.
|
|
155
|
+
- **Zone** — physical zone e.g. `Master`, `BnB`. Used in MQTT topics.
|
|
166
156
|
- **Broker** — select your existing MQTT broker config
|
|
157
|
+
- **DMX Floor** — minimum DMX value sent to controllers (default 3). Prevents flicker on decoders that are unstable at values 1–2. Set to 0 to disable.
|
|
158
|
+
- **Default ON transition** — fade time in seconds when HA sends no transition (default 1s)
|
|
159
|
+
- **Default OFF transition** — fade time in seconds when HA turns off with no transition (default 1s)
|
|
167
160
|
4. Click **Add**
|
|
168
161
|
|
|
169
162
|
### 2. Add a DMX fixture
|
|
@@ -172,7 +165,7 @@ Fill in the node editor:
|
|
|
172
165
|
1. **Fixture ID** — Prefix (`L`), Plan ID, optional channel letter e.g. `L` `992` `-A`
|
|
173
166
|
2. **Colour Mode** — RGBW, RGB, CCT etc.
|
|
174
167
|
3. **Device Type** — e.g. Downlight, Strip light
|
|
175
|
-
4. **Situation** — describes where the fixture is relative to its area
|
|
168
|
+
4. **Situation** — describes where the fixture is relative to its area:
|
|
176
169
|
|
|
177
170
|
| Situation | Example result |
|
|
178
171
|
|---|---|
|
|
@@ -184,19 +177,90 @@ Fill in the node editor:
|
|
|
184
177
|
| `outside` | Rail light **outside** Media Room |
|
|
185
178
|
| `throughout` | Downlight **throughout** Living Area |
|
|
186
179
|
|
|
187
|
-
> **Tip:** Don't default to `in` for everything — `outside`, `near` and `above` are useful for fixtures in corridors, at thresholds, and above architectural features. On a large installation this makes fixture names immediately meaningful without opening a plan.
|
|
188
|
-
|
|
189
180
|
5. **Config** — select your config node
|
|
190
181
|
6. **Area** and **Sub-Area** — location on the property
|
|
191
182
|
7. **DMX Channels** — channel numbers matching your fixture's DMX start address
|
|
192
183
|
8. **Controller** and **Universe** numbers
|
|
193
|
-
9. **
|
|
184
|
+
9. **DMX Floor** — leave blank to use the config node default, or set a value to override for this fixture only
|
|
185
|
+
10. **Discovery Mode** — Enabled, Hidden, or Disabled
|
|
194
186
|
|
|
195
187
|
Deploy — the fixture appears in HA automatically.
|
|
196
188
|
|
|
197
|
-
|
|
189
|
+
---
|
|
190
|
+
|
|
191
|
+
## DMX Floor
|
|
198
192
|
|
|
199
|
-
|
|
193
|
+
The DMX floor sets the **absolute minimum value** sent to the controller for any channel that is on. It prevents flickering on LED decoders that are unstable at values 1 or 2.
|
|
194
|
+
|
|
195
|
+
```
|
|
196
|
+
value = 0 → sends 0 (OFF — always honoured)
|
|
197
|
+
0 < value < floor → sends floor (snapped up)
|
|
198
|
+
value >= floor → sends value (unchanged)
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
This is enforced at the output stage — no code path can bypass it.
|
|
202
|
+
|
|
203
|
+
**Config node** sets the site-wide default (e.g. 3). **DMX node** can override it per fixture — leave blank to inherit from config, or set a specific value for fixtures that can handle lower values without flicker.
|
|
204
|
+
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
## DMX Group Node
|
|
208
|
+
|
|
209
|
+
Appears as a single light entity in HA. Commands fan out to all member fixtures.
|
|
210
|
+
|
|
211
|
+
### Two ways to define members
|
|
212
|
+
|
|
213
|
+
**Method A — Wires (v1, still supported):**
|
|
214
|
+
```
|
|
215
|
+
[DMX Group LG-992]
|
|
216
|
+
│ Link output
|
|
217
|
+
├──→ [DMX L-992-A]
|
|
218
|
+
├──→ [DMX L-992-B]
|
|
219
|
+
└──→ [DMX L-992-C]
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
**Method B — Member list (v2, recommended):**
|
|
223
|
+
|
|
224
|
+
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.
|
|
225
|
+
|
|
226
|
+
```
|
|
227
|
+
Members:
|
|
228
|
+
local L-992-A → forwarded via RED.nodes.getNode() internally
|
|
229
|
+
local L-992-B → forwarded via RED.nodes.getNode() internally
|
|
230
|
+
local P-149 → relay node, forwarded internally
|
|
231
|
+
remote MW3D/BnB/group/cmd → published via MQTT for cross-instance groups
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
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.
|
|
235
|
+
|
|
236
|
+
### Member types
|
|
237
|
+
|
|
238
|
+
| Type | Value | How it works |
|
|
239
|
+
|---|---|---|
|
|
240
|
+
| `local` | Fixture ID e.g. `L-992-A` | Resolved via fixture registry → `RED.nodes.getNode()` |
|
|
241
|
+
| `remote` | MQTT topic e.g. `MW3D/BnB/group/cmd` | Published via MQTT with loop-detection envelope |
|
|
242
|
+
|
|
243
|
+
### Duplicate member protection
|
|
244
|
+
|
|
245
|
+
If the same fixture ID appears more than once in the member list, the **entire group command is blocked** and an orange warning is shown. The editor also prevents saving with duplicates.
|
|
246
|
+
|
|
247
|
+
### Loop detection
|
|
248
|
+
|
|
249
|
+
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.
|
|
250
|
+
|
|
251
|
+
### Node status
|
|
252
|
+
|
|
253
|
+
| Colour | Meaning |
|
|
254
|
+
|---|---|
|
|
255
|
+
| Green dot | Healthy — all members resolved |
|
|
256
|
+
| Yellow ring | One or more members not found in registry |
|
|
257
|
+
| Orange ring | Duplicate member detected — group blocked |
|
|
258
|
+
| Red dot | Loop detected |
|
|
259
|
+
| Red ring | Broker disconnected |
|
|
260
|
+
|
|
261
|
+
### Naming convention
|
|
262
|
+
|
|
263
|
+
`L-992-A`, `L-992-B`, `L-992-C` group naturally under `LG-992`. The `LG` prefix is fixed.
|
|
200
264
|
|
|
201
265
|
---
|
|
202
266
|
|
|
@@ -230,8 +294,6 @@ Typical workflow: Remove → update node settings → Deploy → node auto-disco
|
|
|
230
294
|
|
|
231
295
|
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.
|
|
232
296
|
|
|
233
|
-
This is one of the most useful commissioning features in the package. On large installations with hundreds of entities, the ability to wipe and rediscover a specific zone or type in one click saves hours of manual work.
|
|
234
|
-
|
|
235
297
|
**Topic:** `{siteId}/system/control` (e.g. `MW3D/system/control`)
|
|
236
298
|
|
|
237
299
|
**Payload:**
|
|
@@ -247,116 +309,25 @@ This is one of the most useful commissioning features in the package. On large i
|
|
|
247
309
|
| `zone` | `all`, or any zone name e.g. `Master`, `BnB` | Matches config node Zone field — case insensitive |
|
|
248
310
|
| `type` | `all`, `dmx`, `group`, `button`, `pir`, `relay` | Node type filter |
|
|
249
311
|
|
|
250
|
-
**
|
|
251
|
-
|
|
252
|
-
| Type | Node | Description |
|
|
253
|
-
|---|---|---|
|
|
254
|
-
| `dmx` | ha-mqtt-dmx | Individual DMX fixtures |
|
|
255
|
-
| `group` | ha-mqtt-dmx-group | DMX group nodes |
|
|
256
|
-
| `button` | ha-mqtt-button | Wall button inputs |
|
|
257
|
-
| `pir` | ha-mqtt-pir | PIR / motion sensors |
|
|
258
|
-
| `relay` | ha-mqtt-relay | Relay / power outputs |
|
|
259
|
-
| `all` | all of the above | Everything in the zone |
|
|
260
|
-
|
|
261
|
-
**Commissioning workflow — roll out one type at a time:**
|
|
262
|
-
```
|
|
263
|
-
Day 1 — DMX lights:
|
|
264
|
-
ADD Master DMX → all lights appear in HA
|
|
265
|
-
Test each light
|
|
266
|
-
REMOVE Master DMX → wipe and fix any wrong names/channels
|
|
267
|
-
ADD Master DMX → rediscover clean
|
|
268
|
-
|
|
269
|
-
Day 2 — Buttons:
|
|
270
|
-
ADD Master Button → all buttons appear in HA
|
|
271
|
-
Test each button
|
|
272
|
-
etc.
|
|
273
|
-
```
|
|
274
|
-
|
|
275
|
-
**How to send the command:**
|
|
276
|
-
|
|
277
|
-
Option A — from anywhere on the network (MQTT Explorer, HA automation, script):
|
|
312
|
+
**Commissioning workflow:**
|
|
278
313
|
```
|
|
279
|
-
|
|
280
|
-
|
|
314
|
+
ADD Master DMX → all lights appear in HA → test each light
|
|
315
|
+
REMOVE Master DMX → wipe and fix any wrong names/channels
|
|
316
|
+
ADD Master DMX → rediscover clean
|
|
281
317
|
```
|
|
282
318
|
|
|
283
|
-
Option B — from Node-RED using the included System Control flow:
|
|
284
|
-
|
|
285
|
-
Import `system_control_flow.json` into Node-RED. Connect the MQTT out node to your broker. You get a dedicated tab with one-click buttons for every zone and type combination:
|
|
286
|
-
|
|
287
|
-
```
|
|
288
|
-
🗑 REMOVE ALL ✅ ADD ALL
|
|
289
|
-
|
|
290
|
-
🗑 REMOVE Master DMX ✅ ADD Master DMX
|
|
291
|
-
🗑 REMOVE Master Group ✅ ADD Master Group
|
|
292
|
-
🗑 REMOVE Master Button ✅ ADD Master Button
|
|
293
|
-
🗑 REMOVE Master PIR ✅ ADD Master PIR
|
|
294
|
-
🗑 REMOVE Master Relay ✅ ADD Master Relay
|
|
295
|
-
|
|
296
|
-
🗑 REMOVE BnB DMX ✅ ADD BnB DMX
|
|
297
|
-
... etc.
|
|
298
|
-
```
|
|
299
|
-
|
|
300
|
-
Option C — wire an inject node directly to any ha-mqtt node input:
|
|
301
|
-
```
|
|
302
|
-
msg.device = "add" → add that node only
|
|
303
|
-
msg.device = "remove" → remove that node only
|
|
304
|
-
```
|
|
305
|
-
|
|
306
|
-
> **Tip:** The system control topic is zone and type filtered — sending `REMOVE BnB DMX` only affects BnB DMX fixtures. Master lights, buttons, relays are completely unaffected. This makes it safe to use on a live installation with clients present.
|
|
307
|
-
|
|
308
|
-
---
|
|
309
|
-
|
|
310
|
-
## DMX Group Node
|
|
311
|
-
|
|
312
|
-
Appears as a single light entity in HA. Commands fan out to all nodes on its **Link** output.
|
|
313
|
-
|
|
314
|
-
```
|
|
315
|
-
[DMX Group LG-992]
|
|
316
|
-
│ Link
|
|
317
|
-
├──→ [DMX L-992-A]
|
|
318
|
-
├──→ [DMX L-992-B]
|
|
319
|
-
└──→ [DMX L-992-C]
|
|
320
|
-
```
|
|
321
|
-
|
|
322
|
-
Wire **Link** to child DMX or Relay node inputs. Supports nested groups.
|
|
323
|
-
|
|
324
|
-
Naming: `L-992-A`, `L-992-B`, `L-992-C` group naturally under `LG-992`.
|
|
325
|
-
|
|
326
319
|
---
|
|
327
320
|
|
|
328
321
|
## Wall buttons
|
|
329
322
|
|
|
330
323
|
Creates two HA entities per button:
|
|
331
324
|
|
|
332
|
-
- **`binary_sensor.s_10_a`** — physical state. ON = pressed/held, OFF = released.
|
|
325
|
+
- **`binary_sensor.s_10_a`** — physical state. ON = pressed/held, OFF = released.
|
|
333
326
|
- **`button.s_10_a_btn`** — momentary trigger. Simulates a physical press from the HA UI.
|
|
334
327
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
This package is designed for **professional installation** as well as home use. On large installations it is not practical to physically press every button every time you make a change — the building may be large, the button may be on the other side of the property, or you may be commissioning remotely.
|
|
338
|
-
|
|
339
|
-
The `button` entity gives you a **virtual commissioning panel** — every physical wall button has a software mirror in HA that you can trigger from a laptop anywhere in the world without being on site.
|
|
340
|
-
|
|
341
|
-
### Important — HA button entity behaviour
|
|
342
|
-
|
|
343
|
-
The `button` domain in HA is **intentionally stateless** — it will never light up or show an active state. This is a HA platform design decision, not a bug. The `button` entity triggers and immediately resets with no persistent state.
|
|
328
|
+
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.
|
|
344
329
|
|
|
345
|
-
For visual feedback always use the `binary_sensor` entity.
|
|
346
|
-
|
|
347
|
-
### Recommended dashboard setup
|
|
348
|
-
|
|
349
|
-
Hide the `button` entity from the default dashboard to keep things clean — it remains fully accessible when needed:
|
|
350
|
-
|
|
351
|
-
```
|
|
352
|
-
Settings → Devices → (button device) → button entity → ⋮ → Hide from dashboard
|
|
353
|
-
```
|
|
354
|
-
|
|
355
|
-
This gives you:
|
|
356
|
-
- **Dashboard** — clean, only `binary_sensor` shown
|
|
357
|
-
- **Device page** — both entities visible, Press button accessible for commissioning
|
|
358
|
-
- **Automations** — both entities fully available, nothing hidden from logic
|
|
359
|
-
- **Remote commissioning** — navigate to device page, simulate press from anywhere
|
|
330
|
+
The `button` domain in HA is intentionally stateless — it will never show an active state. For visual feedback always use the `binary_sensor` entity.
|
|
360
331
|
|
|
361
332
|
Letters `I` and `O` are never used as suffixes — they look too similar to `1` and `0`.
|
|
362
333
|
|
|
@@ -383,69 +354,22 @@ Entity IDs in HA are locked to the fixture ID and survive friendly name changes.
|
|
|
383
354
|
|
|
384
355
|
## Retain discovery — why the default is false
|
|
385
356
|
|
|
386
|
-
If you search "MQTT discovery retain" you will find HA community posts
|
|
387
|
-
|
|
388
|
-
### Why ESP devices need retain=true
|
|
389
|
-
|
|
390
|
-
An ESP sensor runs independently of HA. When HA restarts or updates, the ESP device has no way of knowing — it thinks it already discovered itself and won't re-send the discovery payload. Without `retain=true`, the device disappears from HA permanently until the ESP reboots.
|
|
391
|
-
|
|
392
|
-
```
|
|
393
|
-
ESP device Home Assistant
|
|
394
|
-
────────── ──────────────
|
|
395
|
-
Sends discovery once → HA restarts
|
|
396
|
-
Doesn't know HA restarted Retained message restores device ✓
|
|
397
|
-
```
|
|
398
|
-
|
|
399
|
-
### Why Node-RED is different
|
|
400
|
-
|
|
401
|
-
Node-RED sits on the same server stack as HA. They are tightly coupled — if one goes down the other knows immediately via the websocket connection. When HA restarts, NR reconnects and re-discovers every device automatically. If NR goes down, HA *should* show nothing — because nothing is working.
|
|
402
|
-
|
|
403
|
-
```
|
|
404
|
-
Node-RED + Home Assistant (same server):
|
|
405
|
-
NR down → no devices in HA → client sees fault immediately ✓
|
|
406
|
-
HA restart → NR reconnects → re-discovers everything ✓
|
|
407
|
-
retain=true → ghost devices → client confused, support call ✗
|
|
408
|
-
```
|
|
409
|
-
|
|
410
|
-
### The config node setting
|
|
357
|
+
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.**
|
|
411
358
|
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
| Setting | Behaviour | Recommended for |
|
|
415
|
-
|---|---|---|
|
|
416
|
-
| `false` (default) | Devices disappear if NR stops | Professional installs — clear fault indication |
|
|
417
|
-
| `true` | Devices persist if NR stops | Standalone IoT, voice assistant integrations where entity persistence matters |
|
|
418
|
-
|
|
419
|
-
**Default is `false`** — if a client opens their HA dashboard and sees no lights, they know something is wrong with Node-RED. This is the correct behaviour for a professionally installed system where NR and HA live on the same hardware.
|
|
420
|
-
|
|
421
|
-
---
|
|
359
|
+
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.
|
|
422
360
|
|
|
423
|
-
|
|
361
|
+
With `retain=false` (default): if NR stops, entities disappear. Clients see a fault immediately. This is the correct behaviour for a professionally installed system.
|
|
424
362
|
|
|
425
|
-
|
|
|
363
|
+
| Setting | Recommended for |
|
|
426
364
|
|---|---|
|
|
427
|
-
|
|
|
428
|
-
|
|
|
429
|
-
| HA discovery | `homeassistant/light/{fixtureId}/config` |
|
|
430
|
-
|
|
431
|
-
DMX payload format: `"212255"` = channel 212, value 255 (3 digits each).
|
|
432
|
-
|
|
433
|
-
---
|
|
434
|
-
|
|
435
|
-
## Multi-zone deployments
|
|
436
|
-
|
|
437
|
-
Create one `ha-mqtt-config` per zone. Each node selects its zone via the Config dropdown.
|
|
438
|
-
|
|
439
|
-
```
|
|
440
|
-
Master Zone config → Zone: Master → topics: home/Master/dmx/1
|
|
441
|
-
Guest Wing config → Zone: GuestWing → topics: home/GuestWing/dmx/1
|
|
442
|
-
```
|
|
365
|
+
| `false` (default) | Professional installs — clear fault indication |
|
|
366
|
+
| `true` | Standalone IoT where entity persistence matters more than fault clarity |
|
|
443
367
|
|
|
444
368
|
---
|
|
445
369
|
|
|
446
370
|
## Transition rate tuning
|
|
447
371
|
|
|
448
|
-
The config node has two global transition settings
|
|
372
|
+
The config node has two global transition settings:
|
|
449
373
|
|
|
450
374
|
### Transition rate limit
|
|
451
375
|
|
|
@@ -455,151 +379,56 @@ Controls the tick rate multiplier for all transitions and effects:
|
|
|
455
379
|
ticksPerSec (per node) × rateLimit (config) = effectiveTicks/sec
|
|
456
380
|
```
|
|
457
381
|
|
|
458
|
-
| Rate Limit | Effective ticks/sec |
|
|
459
|
-
|
|
460
|
-
| 1.0 | 31 |
|
|
461
|
-
| 0.5 | 15 |
|
|
462
|
-
| 0.25 | 7 |
|
|
463
|
-
| 0.1 | 3 |
|
|
464
|
-
|
|
465
|
-
Reduce `transitionRateLimit` if you observe:
|
|
466
|
-
- MQTT broker lag or dropped messages
|
|
467
|
-
- NR event loop warnings
|
|
468
|
-
- DMX controller missing channel updates
|
|
469
|
-
- Sluggish HA response during scene changes
|
|
382
|
+
| Rate Limit | Effective ticks/sec | Use case |
|
|
383
|
+
|---|---|---|
|
|
384
|
+
| 1.0 | 31 | Small deployment, fast hardware |
|
|
385
|
+
| 0.5 | 15 | Medium deployment, balanced |
|
|
386
|
+
| 0.25 | 7 | Large deployment, light load |
|
|
387
|
+
| 0.1 | 3 | Maximum scale, minimal load |
|
|
470
388
|
|
|
471
|
-
|
|
389
|
+
Reduce `transitionRateLimit` if you see MQTT broker lag, NR event loop warnings, or sluggish HA response during scene changes.
|
|
472
390
|
|
|
473
|
-
|
|
474
|
-
transition specified. Default 1 second.
|
|
391
|
+
### Default transition times
|
|
475
392
|
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
HA sends no transition
|
|
479
|
-
|
|
393
|
+
| Field | Default | Description |
|
|
394
|
+
|---|---|---|
|
|
395
|
+
| Default ON transition | 1s | Fade time when HA sends ON with no transition specified |
|
|
396
|
+
| Default OFF transition | 1s | Fade time when HA sends OFF with no transition specified |
|
|
480
397
|
|
|
481
|
-
Set to `0`
|
|
398
|
+
Set to `0` for instant snap on/off.
|
|
482
399
|
|
|
483
400
|
---
|
|
484
401
|
|
|
485
402
|
## Context store — finding your files
|
|
486
403
|
|
|
487
|
-
|
|
404
|
+
The NR add-on runs in its own Docker container. Context store files are **not** accessible via Samba.
|
|
488
405
|
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
The NR add-on runs in its own Docker container with an isolated config directory. This is **separate** from the main HA config you see via Samba.
|
|
492
|
-
|
|
493
|
-
| What you see | Actual path on host |
|
|
406
|
+
| Location | Path |
|
|
494
407
|
|---|---|
|
|
495
|
-
| Samba `\\{haip}\config\` | HA config — flows, automations etc. |
|
|
496
408
|
| NR add-on config | `/addon_configs/a0d7b954_nodered/` |
|
|
497
409
|
| Context store files | `/addon_configs/a0d7b954_nodered/nodeRED/context_stores/context/` |
|
|
498
410
|
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
### How to find your context files
|
|
502
|
-
|
|
503
|
-
Use the **Studio Code Server** add-on (VS Code in HA):
|
|
504
|
-
|
|
505
|
-
1. Install Studio Code Server from the add-on store if not already installed
|
|
506
|
-
2. Open it: Settings → Add-ons → Studio Code Server → Open Web UI
|
|
507
|
-
3. Open a terminal: Terminal → New Terminal
|
|
508
|
-
4. Run:
|
|
509
|
-
|
|
510
|
-
```bash
|
|
511
|
-
find / -name "*.json" -path "*context*" 2>/dev/null | grep -v proc
|
|
512
|
-
```
|
|
513
|
-
|
|
514
|
-
You should see something like:
|
|
515
|
-
```
|
|
516
|
-
/addon_configs/a0d7b954_nodered/nodeRED/context_stores/context/7e2467f26a0693a0/dd6582b967f0314a.json
|
|
517
|
-
/addon_configs/a0d7b954_nodered/nodeRED/context_stores/context/7e2467f26a0693a0/75b10249fcbfd224.json
|
|
518
|
-
...
|
|
519
|
-
```
|
|
520
|
-
|
|
521
|
-
One `.json` file per node, named by node ID.
|
|
522
|
-
|
|
523
|
-
### Reading a context file
|
|
524
|
-
|
|
525
|
-
```bash
|
|
526
|
-
cat "/addon_configs/a0d7b954_nodered/nodeRED/context_stores/context/{flowId}/{nodeId}.json"
|
|
527
|
-
```
|
|
528
|
-
|
|
529
|
-
A healthy file looks like:
|
|
530
|
-
```json
|
|
531
|
-
{
|
|
532
|
-
"state": "ON",
|
|
533
|
-
"brightness": 255,
|
|
534
|
-
"red": 255,
|
|
535
|
-
"green": 255,
|
|
536
|
-
"blue": 255,
|
|
537
|
-
"white": 255,
|
|
538
|
-
"warmWhite": 0
|
|
539
|
-
}
|
|
540
|
-
```
|
|
541
|
-
|
|
542
|
-
### Important: NR adds a `/context/` subfolder
|
|
543
|
-
|
|
544
|
-
When you set `dir` in your context store config, NR automatically appends `/context/` to it. So:
|
|
545
|
-
|
|
546
|
-
```javascript
|
|
547
|
-
// You set:
|
|
548
|
-
config: { dir: '/config/nodeRED/context_stores' }
|
|
549
|
-
|
|
550
|
-
// NR actually writes to:
|
|
551
|
-
// /config/nodeRED/context_stores/context/
|
|
552
|
-
```
|
|
411
|
+
Use **Studio Code Server** add-on to access these files. One `.json` file per node, named by node ID.
|
|
553
412
|
|
|
554
413
|
### Recommended config.js settings
|
|
555
414
|
|
|
556
415
|
```javascript
|
|
557
416
|
contextStorage: {
|
|
558
417
|
memory: { module: 'memory' },
|
|
559
|
-
disk_values: {
|
|
560
|
-
module: 'localfilesystem',
|
|
561
|
-
config: {
|
|
562
|
-
dir: '/config/nodeRED/context_stores',
|
|
563
|
-
flushInterval: 5 // Write to disk every 5 seconds (default is 30)
|
|
564
|
-
}
|
|
565
|
-
},
|
|
566
|
-
disk_meta: {
|
|
567
|
-
module: 'localfilesystem',
|
|
568
|
-
config: {
|
|
418
|
+
disk_values: {
|
|
419
|
+
module: 'localfilesystem',
|
|
420
|
+
config: {
|
|
569
421
|
dir: '/config/nodeRED/context_stores',
|
|
570
422
|
flushInterval: 5
|
|
571
|
-
}
|
|
423
|
+
}
|
|
572
424
|
},
|
|
573
425
|
default: { module: 'memory' },
|
|
574
426
|
},
|
|
575
427
|
```
|
|
576
428
|
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
`flushInterval: 5` means NR writes to disk every 5 seconds instead of the default 30. This reduces the window of data loss on unexpected shutdown — after turning a light on, wait 5 seconds before restarting.
|
|
429
|
+
Use an **absolute path** starting with `/config/`. A relative path will silently write to the wrong location.
|
|
580
430
|
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
After saving a state and waiting 5+ seconds, check your files appeared:
|
|
584
|
-
|
|
585
|
-
```bash
|
|
586
|
-
ls "/addon_configs/a0d7b954_nodered/nodeRED/context_stores/context/"
|
|
587
|
-
```
|
|
588
|
-
|
|
589
|
-
If the directory is empty or doesn't exist — check your `config.js` path is absolute and restart NR.
|
|
590
|
-
|
|
591
|
-
### NR startup log confirmation
|
|
592
|
-
|
|
593
|
-
On every startup NR logs which context stores loaded:
|
|
594
|
-
|
|
595
|
-
```
|
|
596
|
-
Context store : 'memory' [module=memory]
|
|
597
|
-
Context store : 'disk_values' [module=localfilesystem]
|
|
598
|
-
Context store : 'disk_meta' [module=localfilesystem]
|
|
599
|
-
Context store : 'default' [module=memory]
|
|
600
|
-
```
|
|
601
|
-
|
|
602
|
-
If `disk_values` shows `[module=memory]` — your config.js change didn't take effect.
|
|
431
|
+
`flushInterval: 5` writes to disk every 5 seconds instead of the default 30 — reduces the window of data loss on unexpected shutdown.
|
|
603
432
|
|
|
604
433
|
---
|
|
605
434
|
|
|
@@ -617,44 +446,35 @@ If `disk_values` shows `[module=memory]` — your config.js change didn't take e
|
|
|
617
446
|
|
|
618
447
|
**PIR stuck offline** — Warm-up timer running. Wait for configured warm-up period.
|
|
619
448
|
|
|
449
|
+
**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.
|
|
450
|
+
|
|
451
|
+
**Group command blocked (orange warning)** — Duplicate member in the list. Open the group node editor and remove the duplicate.
|
|
452
|
+
|
|
620
453
|
---
|
|
621
454
|
|
|
622
455
|
## Version history
|
|
623
456
|
|
|
624
457
|
| Version | Changes |
|
|
625
458
|
|---|---|
|
|
626
|
-
| 0.
|
|
627
|
-
| 0.
|
|
628
|
-
| 0.
|
|
459
|
+
| 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. |
|
|
460
|
+
| 0.6.30 | Debug timer leak fixed — S._debugTimer saved and cleared on node close (all 5 nodes) |
|
|
461
|
+
| 0.6.29 | Node label updated to match HA device format including subLocation |
|
|
462
|
+
| 0.6.28 | Per-node DMX floor override field — blank inherits config default |
|
|
463
|
+
| 0.6.27 | DMX floor enforced at sendDmxChannels level — absolute gate, covers all code paths |
|
|
464
|
+
| 0.6.26 | pubState recovery + restoreAfterEffect floor fix (reverted — wrong approach) |
|
|
465
|
+
| 0.6.25 | pubState handleON floor fix (reverted — wrong approach) |
|
|
466
|
+
| 0.6.24 | User published manually |
|
|
467
|
+
| 0.6.23 | colorValue>=3 bleed guard, ON/OFF transition UI fields (0-60s), brightness=0 hard zero restored |
|
|
468
|
+
| 0.6.22 | Transition floor in runTransition, default OFF transition config field |
|
|
469
|
+
| 0.6.21 | Reverted bad rgbw_color parsing block |
|
|
470
|
+
| 0.6.20 | colorValue>=3 guard added, ON transition UI field |
|
|
471
|
+
| 0.6.19 | unregisterFixtureId() on close (all 5 nodes), setMaxListeners(0), brightness=0 hard zero |
|
|
472
|
+
| 0.6.17 | Group node separated from DMX type, README updated |
|
|
473
|
+
| 0.4.4 | Recovery status shows actual state on canvas |
|
|
474
|
+
| 0.4.3 | device:remove no longer clears disk state |
|
|
475
|
+
| 0.4.2 | Group node pubState includes color |
|
|
629
476
|
| 0.4.1 | Group node status reflects ON/OFF correctly |
|
|
630
|
-
| 0.4.0 | Group recovery no longer forwards state to children
|
|
631
|
-
| 0.3.9 | Four fixes: group device:add forward removed, cooldown warn suppressed, group status permanent, disk flush on close |
|
|
632
|
-
| 0.3.8 | Restored missing variable declarations wiped by debug block corruption |
|
|
633
|
-
| 0.3.7 | Fix _debugId computed from S (not fixtureId) to avoid ReferenceError on startup |
|
|
634
|
-
| 0.3.6 | DMX channel conflict detection, jitter timer tracked and cancellable |
|
|
635
|
-
| 0.3.5 | Fix debug mode block initialisation order (S before debug) |
|
|
636
|
-
| 0.3.4 | Debug hint text updated — notes 12hr auto-disable |
|
|
637
|
-
| 0.3.3 | Debug mode canvas warning + 12hr auto-disable safety net |
|
|
638
|
-
| 0.3.2 | Debug output to NR debug tab via node.warn with [DEBUG] prefix |
|
|
639
|
-
| 0.3.1 | Double-fire fix — 5s cooldown + _discovered resets on broker close |
|
|
640
|
-
| 0.3.0 | Debug mode toggle in Advanced section on all nodes |
|
|
641
|
-
| 0.2.9 | _lastSent cache — skip redundant MQTT publishes (unchanged values) |
|
|
642
|
-
| 0.2.8 | Transition jitter, micro-transition threshold, unconfigured ch skip |
|
|
643
|
-
| 0.2.7 | transitionRateLimit + transitionHaUiTime wired from config node |
|
|
644
|
-
| 0.2.6 | Zone optional — omitted from topic when blank |
|
|
645
|
-
| 0.2.5 | buildLocation helper — strips parens, skips duplicates, uses " - " separator |
|
|
646
|
-
| 0.2.4 | sw_version reads from package.json, unique_id = fixtureId only, MW3D removed |
|
|
647
|
-
| 0.2.3 | Canvas label format: L-991-E · Downlight in Bedroom 1 |
|
|
648
|
-
| 0.2.2 | Group Node status updates, broker feedback, version in editor panel |
|
|
649
|
-
| 0.2.1 | Hidden mode — entity not auto-placed in dashboard |
|
|
650
|
-
| 0.2.0 | Canvas button toggles remove/add, auto-discovery on deploy |
|
|
651
|
-
| 0.1.9 | Correct disk store name, auto-discovery fallback, node ordering |
|
|
652
|
-
| 0.1.8 | Section reordering all nodes, Config picker position |
|
|
653
|
-
| 0.1.7 | Discovery Mode, broker already-connected fix |
|
|
654
|
-
| 0.1.6 | Canvas button fix |
|
|
655
|
-
| 0.1.5 | Context store error suppressed |
|
|
656
|
-
| 0.1.2 | Original subflow colours restored |
|
|
657
|
-
| 0.1.1 | UI fixes, postfix alphabet, Config Label, Discovery Mode |
|
|
477
|
+
| 0.4.0 | Group recovery no longer forwards state to children |
|
|
658
478
|
| 0.1.0 | Initial release |
|
|
659
479
|
|
|
660
480
|
---
|
|
@@ -663,6 +483,14 @@ If `disk_values` shows `[module=memory]` — your config.js change didn't take e
|
|
|
663
483
|
|
|
664
484
|
DeSwaggy — Discord: @deswaggy
|
|
665
485
|
|
|
666
|
-
##
|
|
486
|
+
## Licence
|
|
487
|
+
|
|
488
|
+
This package is licensed under **[GPL v3](https://www.gnu.org/licenses/gpl-3.0.txt)**.
|
|
489
|
+
|
|
490
|
+
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.
|
|
667
491
|
|
|
668
|
-
|
|
492
|
+
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.
|
|
493
|
+
|
|
494
|
+
---
|
|
495
|
+
|
|
496
|
+
> **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.
|
|
@@ -313,7 +313,7 @@
|
|
|
313
313
|
</div>
|
|
314
314
|
|
|
315
315
|
<div style="margin-top:16px; padding-top:8px; border-top:1px solid #444; color:#666; font-size:0.8em; text-align:right;">
|
|
316
|
-
node-red-contrib-dmx-for-ha v0.6.
|
|
316
|
+
node-red-contrib-dmx-for-ha v0.6.33
|
|
317
317
|
</div>
|
|
318
318
|
|
|
319
319
|
</script>
|
package/nodes/ha-mqtt-button.js
CHANGED
|
@@ -137,7 +137,7 @@ module.exports = function (RED) {
|
|
|
137
137
|
if (S.debugMode) {
|
|
138
138
|
setStatus('red', 'dot', `ha-mqtt-button "${_debugId}" ⚠ DEBUG MODE ON`);
|
|
139
139
|
node.warn(`[DEBUG] ha-mqtt-button "${_debugId}" — debug mode is enabled. Disable in production.`);
|
|
140
|
-
setTimeout(function () {
|
|
140
|
+
S._debugTimer = setTimeout(function () {
|
|
141
141
|
if (S.debugMode) {
|
|
142
142
|
S.debugMode = false;
|
|
143
143
|
node.warn(`[DEBUG] ha-mqtt-button "${_debugId}" — debug mode auto-disabled after 12 hours`);
|
|
@@ -171,11 +171,14 @@ module.exports = function (RED) {
|
|
|
171
171
|
const regKey = `dmx_fixture_ids_${cfg.siteId}`;
|
|
172
172
|
let reg = {};
|
|
173
173
|
try { reg = globalCtx.get(regKey) || {}; } catch(e) {}
|
|
174
|
-
|
|
175
|
-
|
|
174
|
+
// Migration: handle old string format and new object format
|
|
175
|
+
const _existing = reg[fixtureId];
|
|
176
|
+
const _existingNodeId = _existing && typeof _existing === 'object' ? _existing.nodeId : _existing;
|
|
177
|
+
if (_existingNodeId && _existingNodeId !== node.id) {
|
|
178
|
+
node.warn(`${fixtureId} ⚠ DUPLICATE FIXTURE ID — already registered by node $_{_existingNodeId}`);
|
|
176
179
|
setStatus('red', 'dot', `DUPLICATE ID: ${fixtureId}`);
|
|
177
180
|
} else {
|
|
178
|
-
reg[fixtureId] = node.id;
|
|
181
|
+
reg[fixtureId] = { nodeId: node.id, nodeType: 'ha-mqtt-button' };
|
|
179
182
|
try { globalCtx.set(regKey, reg); } catch(e) {}
|
|
180
183
|
}
|
|
181
184
|
}
|
|
@@ -184,7 +187,9 @@ module.exports = function (RED) {
|
|
|
184
187
|
const regKey = `dmx_fixture_ids_${cfg.siteId}`;
|
|
185
188
|
let reg = {};
|
|
186
189
|
try { reg = globalCtx.get(regKey) || {}; } catch(e) {}
|
|
187
|
-
|
|
190
|
+
const _unreg = reg[fixtureId];
|
|
191
|
+
const _unregNodeId = _unreg && typeof _unreg === 'object' ? _unreg.nodeId : _unreg;
|
|
192
|
+
if (_unregNodeId === node.id) {
|
|
188
193
|
delete reg[fixtureId];
|
|
189
194
|
try { globalCtx.set(regKey, reg); } catch(e) {}
|
|
190
195
|
}
|
|
@@ -315,6 +320,7 @@ module.exports = function (RED) {
|
|
|
315
320
|
|
|
316
321
|
// ── Cleanup ───────────────────────────────────────────────
|
|
317
322
|
node.on('close', function (done) {
|
|
323
|
+
if (S._debugTimer) clearTimeout(S._debugTimer);
|
|
318
324
|
broker.unsubscribe(S._activeBtnTopic || S.subscribeTopic, node.id);
|
|
319
325
|
broker.unsubscribe(uiBtnCmdTopic, node.id);
|
|
320
326
|
broker.deregister(node, done);
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
availTopic: { value: 'avty' },
|
|
23
23
|
enabledDefault: { value: 'true' },
|
|
24
24
|
diskDelay: { value: '10' },
|
|
25
|
-
transitionRateLimit: { value:
|
|
25
|
+
transitionRateLimit: { value: 1, validate: RED.validators.number() },
|
|
26
26
|
transitionHaUiTime: { value: 1, validate: RED.validators.number() },
|
|
27
27
|
offTransition: { value: 1, validate: RED.validators.number() },
|
|
28
28
|
flashShort: { value: '1' },
|
|
@@ -228,6 +228,17 @@
|
|
|
228
228
|
</span>
|
|
229
229
|
</div>
|
|
230
230
|
|
|
231
|
+
<div class="form-row">
|
|
232
|
+
<label for="node-config-input-transitionRateLimit">
|
|
233
|
+
<i class="fa fa-tachometer"></i> Transition rate limit
|
|
234
|
+
</label>
|
|
235
|
+
<input type="number" id="node-config-input-transitionRateLimit"
|
|
236
|
+
min="0.1" max="1" step="0.05" style="width:70px" />
|
|
237
|
+
<span style="margin-left:8px; color:#999; font-size:0.85em;">
|
|
238
|
+
Multiplier for transition tick rate (0.1–1.0). Lower = fewer MQTT messages. Default: 1
|
|
239
|
+
</span>
|
|
240
|
+
</div>
|
|
241
|
+
|
|
231
242
|
<div class="form-row">
|
|
232
243
|
<label style="width:100%; font-weight:bold; color:#999; font-size:0.85em; text-transform:uppercase; letter-spacing:0.05em;">
|
|
233
244
|
<i class="fa fa-sliders"></i> DMX Channel Limits
|
|
@@ -47,6 +47,8 @@
|
|
|
47
47
|
showEffects: { value: true },
|
|
48
48
|
transitions: { value: true },
|
|
49
49
|
defaultState: { value: 'OFF' },
|
|
50
|
+
// Members
|
|
51
|
+
members: { value: [] },
|
|
50
52
|
// Advanced
|
|
51
53
|
maxDepth: { value: '10' },
|
|
52
54
|
debugMode: { value: false },
|
|
@@ -55,17 +57,77 @@
|
|
|
55
57
|
label: function () {
|
|
56
58
|
if (this.name) return this.name;
|
|
57
59
|
const id = `LG-${this.uid || '?'}${this.uidPostfix || ''}`;
|
|
58
|
-
const type = this.deviceType
|
|
59
|
-
const sit = this.situation
|
|
60
|
-
const area = this.area
|
|
61
|
-
|
|
60
|
+
const type = this.deviceType || '';
|
|
61
|
+
const sit = this.situation || 'in';
|
|
62
|
+
const area = this.area || '';
|
|
63
|
+
const sub = this.subLocation || '';
|
|
64
|
+
const loc = sub ? `${area} - ${sub}` : area;
|
|
65
|
+
return `(${id}) - ${type} Group ${sit} the ${loc}`.trim();
|
|
62
66
|
},
|
|
63
67
|
|
|
64
68
|
labelStyle: function () {
|
|
65
69
|
return this.name ? 'node_label_italic' : '';
|
|
66
70
|
},
|
|
67
71
|
|
|
72
|
+
oneditsave: function () {
|
|
73
|
+
const members = [];
|
|
74
|
+
const seen = {};
|
|
75
|
+
let hasDuplicate = false;
|
|
76
|
+
let duplicateVal = '';
|
|
77
|
+
|
|
78
|
+
$('#node-input-members-list').editableList('items').each(function () {
|
|
79
|
+
const type = $(this).find('select').val();
|
|
80
|
+
const value = $(this).find('input').val().trim();
|
|
81
|
+
if (!value) return;
|
|
82
|
+
const key = type + ':' + value;
|
|
83
|
+
if (seen[key]) { hasDuplicate = true; duplicateVal = value; }
|
|
84
|
+
seen[key] = true;
|
|
85
|
+
members.push({ type, value });
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
if (hasDuplicate) {
|
|
89
|
+
RED.notify(`Duplicate member "${duplicateVal}" — please remove the duplicate before saving.`, 'error');
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
this.members = members;
|
|
93
|
+
},
|
|
94
|
+
|
|
68
95
|
oneditprepare: function () {
|
|
96
|
+
const node = this;
|
|
97
|
+
|
|
98
|
+
// ── Member list (editableList) ────────────────────────
|
|
99
|
+
$('#node-input-members-list').editableList({
|
|
100
|
+
addItem: function (container, i, data) {
|
|
101
|
+
data = data || { type: 'local', value: '' };
|
|
102
|
+
container.css({ display: 'flex', gap: '8px', alignItems: 'center', width: '100%' });
|
|
103
|
+
|
|
104
|
+
const typeSelect = $('<select style="width:90px; flex-shrink:0;">')
|
|
105
|
+
.append($('<option value="local">local</option>'))
|
|
106
|
+
.append($('<option value="remote">remote</option>'))
|
|
107
|
+
.val(data.type || 'local');
|
|
108
|
+
|
|
109
|
+
const valueInput = $('<input type="text" style="flex:1; min-width:0;">')
|
|
110
|
+
.attr('placeholder', data.type === 'remote' ? 'e.g. MW3D/BnB/group/cmd' : 'e.g. L-177-A')
|
|
111
|
+
.val(data.value || '');
|
|
112
|
+
|
|
113
|
+
typeSelect.on('change', function () {
|
|
114
|
+
const t = $(this).val();
|
|
115
|
+
valueInput.attr('placeholder', t === 'remote' ? 'e.g. MW3D/BnB/group/cmd' : 'e.g. L-177-A');
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
container.append(typeSelect).append(valueInput);
|
|
119
|
+
},
|
|
120
|
+
removable: true,
|
|
121
|
+
sortable: true,
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// Populate existing members
|
|
125
|
+
const members = node.members || [];
|
|
126
|
+
members.forEach(function (m) {
|
|
127
|
+
$('#node-input-members-list').editableList('addItem', m);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// ── Area/sub-area lists ───────────────────────────────
|
|
69
131
|
// Reuse area/sub-area lists from ha-mqtt-dmx if loaded,
|
|
70
132
|
// otherwise define inline
|
|
71
133
|
const areas = typeof HA_DMX_AREAS !== 'undefined' ? HA_DMX_AREAS : [
|
|
@@ -260,6 +322,22 @@
|
|
|
260
322
|
|
|
261
323
|
<hr/>
|
|
262
324
|
|
|
325
|
+
<div class="form-row">
|
|
326
|
+
<label style="width:100%; font-weight:bold; color:#999; font-size:0.85em; text-transform:uppercase; letter-spacing:0.05em;">
|
|
327
|
+
<i class="fa fa-list"></i> Members
|
|
328
|
+
</label>
|
|
329
|
+
</div>
|
|
330
|
+
|
|
331
|
+
<div class="form-row">
|
|
332
|
+
<div style="margin-left:0; color:#999; font-size:0.85em; margin-bottom:8px;">
|
|
333
|
+
Add fixtures by ID (local) or MQTT topic (remote). Wires still work alongside this list.
|
|
334
|
+
Duplicate members will block the entire group command.
|
|
335
|
+
</div>
|
|
336
|
+
<ol id="node-input-members-list" style="width:100%;"></ol>
|
|
337
|
+
</div>
|
|
338
|
+
|
|
339
|
+
<hr/>
|
|
340
|
+
|
|
263
341
|
<div class="form-row">
|
|
264
342
|
<label style="width:100%; font-weight:bold; color:#999; font-size:0.85em; text-transform:uppercase; letter-spacing:0.05em;">
|
|
265
343
|
<i class="fa fa-sliders"></i> Options
|
|
@@ -317,7 +395,7 @@
|
|
|
317
395
|
</div>
|
|
318
396
|
|
|
319
397
|
<div style="margin-top:16px; padding-top:8px; border-top:1px solid #444; color:#666; font-size:0.8em; text-align:right;">
|
|
320
|
-
node-red-contrib-dmx-for-ha v0.6.
|
|
398
|
+
node-red-contrib-dmx-for-ha v0.6.33
|
|
321
399
|
</div>
|
|
322
400
|
|
|
323
401
|
</script>
|
|
@@ -328,9 +406,9 @@
|
|
|
328
406
|
============================================================ -->
|
|
329
407
|
<script type="text/html" data-help-name="ha-mqtt-dmx-group">
|
|
330
408
|
<p>
|
|
331
|
-
Virtual DMX group. Appears as a single light entity in Home Assistant.
|
|
332
|
-
Commands
|
|
333
|
-
|
|
409
|
+
Virtual DMX group node (v2). Appears as a single light entity in Home Assistant.
|
|
410
|
+
Commands are forwarded to member fixtures via the internal NR message bus (no extra MQTT traffic).
|
|
411
|
+
Wires and member list can be used together or independently.
|
|
334
412
|
</p>
|
|
335
413
|
|
|
336
414
|
<h3>Setup</h3>
|
|
@@ -338,10 +416,26 @@
|
|
|
338
416
|
<li>Select your <strong>Config</strong> node</li>
|
|
339
417
|
<li>Set a <strong>Group Name</strong> — shown as the HA entity friendly name</li>
|
|
340
418
|
<li>Set <strong>Group ID</strong> — matches the plan ID (prefix LG is fixed)</li>
|
|
341
|
-
<li>
|
|
419
|
+
<li>Add members to the <strong>Members</strong> list by fixture ID (local) or MQTT topic (remote)</li>
|
|
420
|
+
<li>Optionally wire the <strong>Link</strong> output to additional nodes</li>
|
|
342
421
|
<li>Deploy — the group entity appears in HA</li>
|
|
343
422
|
</ol>
|
|
344
423
|
|
|
424
|
+
<h3>Members list</h3>
|
|
425
|
+
<p>
|
|
426
|
+
<strong>Local</strong> members are resolved via the fixture ID registry (e.g. <code>L-177-A</code>, <code>P-149</code>).
|
|
427
|
+
Commands are forwarded internally via <code>RED.nodes.getNode()</code> — no MQTT round trip.
|
|
428
|
+
<strong>Remote</strong> members are MQTT topics for cross-instance groups (e.g. <code>MW3D/BnB/group/cmd</code>).
|
|
429
|
+
Remote commands include a <code>_dmxTrace</code> envelope for cross-instance loop detection.
|
|
430
|
+
</p>
|
|
431
|
+
|
|
432
|
+
<h3>Duplicate members</h3>
|
|
433
|
+
<p>
|
|
434
|
+
Duplicate entries in the member list will block the entire group command and show an orange warning.
|
|
435
|
+
The UI also prevents saving with duplicates. If a fixture ID is in both the member list and wired,
|
|
436
|
+
it will receive the command twice — avoid this by using one method only per fixture.
|
|
437
|
+
</p>
|
|
438
|
+
|
|
345
439
|
<h3>Group ID</h3>
|
|
346
440
|
<p>
|
|
347
441
|
The <code>LG</code> prefix is fixed for all group nodes — it matches the
|
|
@@ -349,18 +443,12 @@
|
|
|
349
443
|
and <code>L-992-C</code> form group <code>LG-992</code>.
|
|
350
444
|
</p>
|
|
351
445
|
|
|
352
|
-
<h3>Link output</h3>
|
|
353
|
-
<p>
|
|
354
|
-
Wire the Link output to one or more DMX or Relay node inputs.
|
|
355
|
-
You can also wire it to another DMX Group Node input to create
|
|
356
|
-
a nested group hierarchy.
|
|
357
|
-
</p>
|
|
358
|
-
|
|
359
446
|
<h3>Loop detection</h3>
|
|
360
447
|
<p>
|
|
361
|
-
The node tracks the cascade path in <code>msg.dmx_trace</code> and
|
|
362
|
-
fires a warning if a loop is detected.
|
|
363
|
-
|
|
448
|
+
The node tracks the cascade path in <code>msg.dmx_trace</code> and <code>_dmxTrace</code>
|
|
449
|
+
and fires a warning if a loop is detected. Works across both wired and member list paths,
|
|
450
|
+
and across instances via the remote MQTT envelope. Max depth sets the maximum hops before
|
|
451
|
+
a loop is assumed. Set to 0 to disable.
|
|
364
452
|
</p>
|
|
365
453
|
|
|
366
454
|
<h3>Inputs</h3>
|
|
@@ -131,6 +131,7 @@ module.exports = function (RED) {
|
|
|
131
131
|
transitions: config.transitions !== false,
|
|
132
132
|
defaultState: config.defaultState || 'OFF',
|
|
133
133
|
maxDepth: parseInt(config.maxDepth) || 10,
|
|
134
|
+
members: Array.isArray(config.members) ? config.members : [],
|
|
134
135
|
flashShort: cfg.flashShort,
|
|
135
136
|
flashLong: cfg.flashLong,
|
|
136
137
|
diskDelay: cfg.diskDelay,
|
|
@@ -142,7 +143,7 @@ module.exports = function (RED) {
|
|
|
142
143
|
if (S.debugMode) {
|
|
143
144
|
setStatus('red', 'dot', `ha-mqtt-dmx-group "${_debugId}" ⚠ DEBUG MODE ON`);
|
|
144
145
|
node.warn(`[DEBUG] ha-mqtt-dmx-group "${_debugId}" — debug mode is enabled. Disable in production.`);
|
|
145
|
-
setTimeout(function () {
|
|
146
|
+
S._debugTimer = setTimeout(function () {
|
|
146
147
|
if (S.debugMode) {
|
|
147
148
|
S.debugMode = false;
|
|
148
149
|
node.warn(`[DEBUG] ha-mqtt-dmx-group "${_debugId}" — debug mode auto-disabled after 12 hours`);
|
|
@@ -160,11 +161,14 @@ module.exports = function (RED) {
|
|
|
160
161
|
const regKey = `dmx_fixture_ids_${cfg.siteId}`;
|
|
161
162
|
let reg = {};
|
|
162
163
|
try { reg = globalCtx.get(regKey) || {}; } catch(e) {}
|
|
163
|
-
|
|
164
|
-
|
|
164
|
+
// Migration: handle old string format and new object format
|
|
165
|
+
const _existing = reg[groupId];
|
|
166
|
+
const _existingNodeId = _existing && typeof _existing === 'object' ? _existing.nodeId : _existing;
|
|
167
|
+
if (_existingNodeId && _existingNodeId !== node.id) {
|
|
168
|
+
node.warn(`${groupId} ⚠ DUPLICATE FIXTURE ID — already registered by node ${_existingNodeId}`);
|
|
165
169
|
setStatus('red', 'dot', `DUPLICATE ID: ${groupId}`);
|
|
166
170
|
} else {
|
|
167
|
-
reg[groupId] = node.id;
|
|
171
|
+
reg[groupId] = { nodeId: node.id, nodeType: 'ha-mqtt-dmx-group' };
|
|
168
172
|
try { globalCtx.set(regKey, reg); } catch(e) {}
|
|
169
173
|
}
|
|
170
174
|
}
|
|
@@ -173,7 +177,9 @@ module.exports = function (RED) {
|
|
|
173
177
|
const regKey = `dmx_fixture_ids_${cfg.siteId}`;
|
|
174
178
|
let reg = {};
|
|
175
179
|
try { reg = globalCtx.get(regKey) || {}; } catch(e) {}
|
|
176
|
-
|
|
180
|
+
const _unreg = reg[groupId];
|
|
181
|
+
const _unregNodeId = _unreg && typeof _unreg === 'object' ? _unreg.nodeId : _unreg;
|
|
182
|
+
if (_unregNodeId === node.id) {
|
|
177
183
|
delete reg[groupId];
|
|
178
184
|
try { globalCtx.set(regKey, reg); } catch(e) {}
|
|
179
185
|
}
|
|
@@ -250,7 +256,104 @@ module.exports = function (RED) {
|
|
|
250
256
|
return { source: groupId, path: [groupId], depth: 1 };
|
|
251
257
|
}
|
|
252
258
|
|
|
253
|
-
// ──
|
|
259
|
+
// ── Member list — cache & resolution ─────────────────────
|
|
260
|
+
const _memberCache = {}; // fixtureId → { nodeId, nodeType }
|
|
261
|
+
|
|
262
|
+
function resolveMember(fixtureId) {
|
|
263
|
+
if (_memberCache[fixtureId]) return _memberCache[fixtureId];
|
|
264
|
+
const globalCtx = node.context().global;
|
|
265
|
+
const regKey = `dmx_fixture_ids_${cfg.siteId}`;
|
|
266
|
+
let reg = {};
|
|
267
|
+
try { reg = globalCtx.get(regKey) || {}; } catch(e) {}
|
|
268
|
+
const entry = reg[fixtureId];
|
|
269
|
+
if (!entry) return null;
|
|
270
|
+
// Migration: handle old string format
|
|
271
|
+
const resolved = typeof entry === 'object'
|
|
272
|
+
? entry
|
|
273
|
+
: { nodeId: entry, nodeType: 'unknown' };
|
|
274
|
+
_memberCache[fixtureId] = resolved;
|
|
275
|
+
return resolved;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function validateMembers() {
|
|
279
|
+
// Check for duplicate fixture IDs in member list
|
|
280
|
+
const seen = {};
|
|
281
|
+
for (const m of S.members) {
|
|
282
|
+
if (m.type !== 'local') continue;
|
|
283
|
+
if (seen[m.value]) return { ok: false, duplicate: m.value };
|
|
284
|
+
seen[m.value] = true;
|
|
285
|
+
}
|
|
286
|
+
// Check for duplicate remote topics
|
|
287
|
+
const seenTopics = {};
|
|
288
|
+
for (const m of S.members) {
|
|
289
|
+
if (m.type !== 'remote') continue;
|
|
290
|
+
if (seenTopics[m.value]) return { ok: false, duplicate: m.value };
|
|
291
|
+
seenTopics[m.value] = true;
|
|
292
|
+
}
|
|
293
|
+
return { ok: true };
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function forwardToMemberList(payload, trace) {
|
|
297
|
+
if (S.members.length === 0) return;
|
|
298
|
+
|
|
299
|
+
// Validate — block entire group on duplicate
|
|
300
|
+
const validation = validateMembers();
|
|
301
|
+
if (!validation.ok) {
|
|
302
|
+
node.warn(`${groupId} — duplicate member "${validation.duplicate}" in list. Command blocked.`);
|
|
303
|
+
setStatus('orange', 'ring', `${groupId} DUPLICATE MEMBER: ${validation.duplicate}`);
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Check for wire/list overlap
|
|
308
|
+
// We can't know wired node IDs here, but we can warn about any local member
|
|
309
|
+
// whose nodeId matches a node that received the message via wire
|
|
310
|
+
// This is handled by the DMX node itself receiving twice — we warn at group level
|
|
311
|
+
// by tracking which fixture IDs we're about to send to
|
|
312
|
+
const localIds = S.members
|
|
313
|
+
.filter(m => m.type === 'local')
|
|
314
|
+
.map(m => m.value);
|
|
315
|
+
|
|
316
|
+
let anyNotFound = false;
|
|
317
|
+
|
|
318
|
+
for (const member of S.members) {
|
|
319
|
+
if (member.type === 'local') {
|
|
320
|
+
const resolved = resolveMember(member.value);
|
|
321
|
+
if (!resolved) {
|
|
322
|
+
node.warn(`${groupId} — member "${member.value}" not found in registry, skipping`);
|
|
323
|
+
anyNotFound = true;
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
// Skip sensor-only node types
|
|
327
|
+
if (resolved.nodeType === 'ha-mqtt-button' || resolved.nodeType === 'ha-mqtt-pir') {
|
|
328
|
+
if (S.debugMode) node.warn(`[DEBUG] ${groupId} — skipping ${member.value} (${resolved.nodeType} is receive-only)`);
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
const targetNode = RED.nodes.getNode(resolved.nodeId);
|
|
332
|
+
if (!targetNode) {
|
|
333
|
+
node.warn(`${groupId} — member "${member.value}" node not found (${resolved.nodeId}), skipping`);
|
|
334
|
+
anyNotFound = true;
|
|
335
|
+
// Invalidate cache entry
|
|
336
|
+
delete _memberCache[member.value];
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
// Forward internally
|
|
340
|
+
if (S.debugMode) node.warn(`[DEBUG] ${groupId} → internal → ${member.value} (${resolved.nodeType})`);
|
|
341
|
+
targetNode.receive({ dmx_trace: trace, payload });
|
|
342
|
+
|
|
343
|
+
} else if (member.type === 'remote') {
|
|
344
|
+
// Forward via MQTT with trace embedded
|
|
345
|
+
const remotePayload = Object.assign({}, payload, { _dmxTrace: trace });
|
|
346
|
+
pub(member.value, remotePayload, false);
|
|
347
|
+
if (S.debugMode) node.warn(`[DEBUG] ${groupId} → remote → ${member.value}`);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (anyNotFound) {
|
|
352
|
+
setStatus('yellow', 'ring', `${groupId} — some members not found`);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// ── Forward to children via Link output + member list ────
|
|
254
357
|
function forwardToChildren(payload, incomingTrace) {
|
|
255
358
|
if (isLoop(incomingTrace)) {
|
|
256
359
|
node.warn(`${groupId} — loop detected! path:[${incomingTrace && incomingTrace.path.join(' → ')}]`);
|
|
@@ -258,7 +361,10 @@ module.exports = function (RED) {
|
|
|
258
361
|
return;
|
|
259
362
|
}
|
|
260
363
|
const trace = buildTrace(incomingTrace);
|
|
364
|
+
// Wire output — backward compatible
|
|
261
365
|
node.send([{ dmx_trace: trace, payload }]);
|
|
366
|
+
// Member list — internal + remote
|
|
367
|
+
forwardToMemberList(payload, trace);
|
|
262
368
|
}
|
|
263
369
|
|
|
264
370
|
// ── Effects ───────────────────────────────────────────────
|
|
@@ -417,6 +523,7 @@ module.exports = function (RED) {
|
|
|
417
523
|
|
|
418
524
|
// ── Cleanup ───────────────────────────────────────────────
|
|
419
525
|
node.on('close', function (done) {
|
|
526
|
+
if (S._debugTimer) clearTimeout(S._debugTimer);
|
|
420
527
|
if (diskTimer) clearTimeout(diskTimer);
|
|
421
528
|
broker.unsubscribe(cmdTopic, node.id);
|
|
422
529
|
broker.deregister(node, done);
|
package/nodes/ha-mqtt-dmx.html
CHANGED
|
@@ -178,11 +178,13 @@
|
|
|
178
178
|
|
|
179
179
|
label: function () {
|
|
180
180
|
if (this.name) return this.name;
|
|
181
|
-
const id
|
|
182
|
-
const type
|
|
183
|
-
const sit
|
|
184
|
-
const area
|
|
185
|
-
|
|
181
|
+
const id = `${this.uidPrefix || 'L'}-${this.uid || '?'}${this.uidPostfix || ''}`;
|
|
182
|
+
const type = this.deviceType || '';
|
|
183
|
+
const sit = this.situation || 'in';
|
|
184
|
+
const area = this.area || '';
|
|
185
|
+
const sub = this.subLocation || '';
|
|
186
|
+
const loc = sub ? `${area} - ${sub}` : area;
|
|
187
|
+
return `(${id}) - ${type} ${sit} the ${loc}`.trim();
|
|
186
188
|
},
|
|
187
189
|
|
|
188
190
|
labelStyle: function () {
|
|
@@ -594,7 +596,7 @@
|
|
|
594
596
|
</div>
|
|
595
597
|
|
|
596
598
|
<div style="margin-top:16px; padding-top:8px; border-top:1px solid #444; color:#666; font-size:0.8em; text-align:right;">
|
|
597
|
-
node-red-contrib-dmx-for-ha v0.6.
|
|
599
|
+
node-red-contrib-dmx-for-ha v0.6.33
|
|
598
600
|
</div>
|
|
599
601
|
|
|
600
602
|
</script>
|
package/nodes/ha-mqtt-dmx.js
CHANGED
|
@@ -172,7 +172,7 @@ module.exports = function (RED) {
|
|
|
172
172
|
if (S.debugMode) {
|
|
173
173
|
setStatus('red', 'dot', `ha-mqtt-dmx "${_debugId}" ⚠ DEBUG MODE ON`);
|
|
174
174
|
node.warn(`[DEBUG] ha-mqtt-dmx "${_debugId}" — debug mode is enabled. Disable in production.`);
|
|
175
|
-
setTimeout(function () {
|
|
175
|
+
S._debugTimer = setTimeout(function () {
|
|
176
176
|
if (S.debugMode) {
|
|
177
177
|
S.debugMode = false;
|
|
178
178
|
node.warn(`[DEBUG] ha-mqtt-dmx "${_debugId}" — debug mode auto-disabled after 12 hours`);
|
|
@@ -653,11 +653,14 @@ module.exports = function (RED) {
|
|
|
653
653
|
const regKey = `dmx_fixture_ids_${cfg.siteId}`;
|
|
654
654
|
let reg = {};
|
|
655
655
|
try { reg = globalCtx.get(regKey) || {}; } catch(e) {}
|
|
656
|
-
|
|
657
|
-
|
|
656
|
+
// Migration: handle old string format and new object format
|
|
657
|
+
const _existing = reg[fixtureId];
|
|
658
|
+
const _existingNodeId = _existing && typeof _existing === 'object' ? _existing.nodeId : _existing;
|
|
659
|
+
if (_existingNodeId && _existingNodeId !== node.id) {
|
|
660
|
+
node.warn(`${fixtureId} ⚠ DUPLICATE FIXTURE ID — already registered by node $_{_existingNodeId}`);
|
|
658
661
|
setStatus('red', 'dot', `DUPLICATE ID: ${fixtureId}`);
|
|
659
662
|
} else {
|
|
660
|
-
reg[fixtureId] = node.id;
|
|
663
|
+
reg[fixtureId] = { nodeId: node.id, nodeType: 'ha-mqtt-dmx' };
|
|
661
664
|
try { globalCtx.set(regKey, reg); } catch(e) {}
|
|
662
665
|
}
|
|
663
666
|
}
|
|
@@ -666,7 +669,9 @@ module.exports = function (RED) {
|
|
|
666
669
|
const regKey = `dmx_fixture_ids_${cfg.siteId}`;
|
|
667
670
|
let reg = {};
|
|
668
671
|
try { reg = globalCtx.get(regKey) || {}; } catch(e) {}
|
|
669
|
-
|
|
672
|
+
const _unreg = reg[fixtureId];
|
|
673
|
+
const _unregNodeId = _unreg && typeof _unreg === 'object' ? _unreg.nodeId : _unreg;
|
|
674
|
+
if (_unregNodeId === node.id) {
|
|
670
675
|
delete reg[fixtureId];
|
|
671
676
|
try { globalCtx.set(regKey, reg); } catch(e) {}
|
|
672
677
|
}
|
|
@@ -881,6 +886,7 @@ module.exports = function (RED) {
|
|
|
881
886
|
|
|
882
887
|
// ── Cleanup ───────────────────────────────────────────────
|
|
883
888
|
node.on('close', function (done) {
|
|
889
|
+
if (S._debugTimer) clearTimeout(S._debugTimer);
|
|
884
890
|
clearChannelRegistry();
|
|
885
891
|
stopEffect();
|
|
886
892
|
if (diskTimer) clearTimeout(diskTimer);
|
package/nodes/ha-mqtt-pir.html
CHANGED
|
@@ -318,7 +318,7 @@
|
|
|
318
318
|
</div>
|
|
319
319
|
|
|
320
320
|
<div style="margin-top:16px; padding-top:8px; border-top:1px solid #444; color:#666; font-size:0.8em; text-align:right;">
|
|
321
|
-
node-red-contrib-dmx-for-ha v0.6.
|
|
321
|
+
node-red-contrib-dmx-for-ha v0.6.33
|
|
322
322
|
</div>
|
|
323
323
|
|
|
324
324
|
</script>
|
package/nodes/ha-mqtt-pir.js
CHANGED
|
@@ -135,7 +135,7 @@ module.exports = function (RED) {
|
|
|
135
135
|
if (S.debugMode) {
|
|
136
136
|
setStatus('red', 'dot', `ha-mqtt-pir "${_debugId}" ⚠ DEBUG MODE ON`);
|
|
137
137
|
node.warn(`[DEBUG] ha-mqtt-pir "${_debugId}" — debug mode is enabled. Disable in production.`);
|
|
138
|
-
setTimeout(function () {
|
|
138
|
+
S._debugTimer = setTimeout(function () {
|
|
139
139
|
if (S.debugMode) {
|
|
140
140
|
S.debugMode = false;
|
|
141
141
|
node.warn(`[DEBUG] ha-mqtt-pir "${_debugId}" — debug mode auto-disabled after 12 hours`);
|
|
@@ -171,11 +171,14 @@ module.exports = function (RED) {
|
|
|
171
171
|
const regKey = `dmx_fixture_ids_${cfg.siteId}`;
|
|
172
172
|
let reg = {};
|
|
173
173
|
try { reg = globalCtx.get(regKey) || {}; } catch(e) {}
|
|
174
|
-
|
|
175
|
-
|
|
174
|
+
// Migration: handle old string format and new object format
|
|
175
|
+
const _existing = reg[fixtureId];
|
|
176
|
+
const _existingNodeId = _existing && typeof _existing === 'object' ? _existing.nodeId : _existing;
|
|
177
|
+
if (_existingNodeId && _existingNodeId !== node.id) {
|
|
178
|
+
node.warn(`${fixtureId} ⚠ DUPLICATE FIXTURE ID — already registered by node $_{_existingNodeId}`);
|
|
176
179
|
setStatus('red', 'dot', `DUPLICATE ID: ${fixtureId}`);
|
|
177
180
|
} else {
|
|
178
|
-
reg[fixtureId] = node.id;
|
|
181
|
+
reg[fixtureId] = { nodeId: node.id, nodeType: 'ha-mqtt-pir' };
|
|
179
182
|
try { globalCtx.set(regKey, reg); } catch(e) {}
|
|
180
183
|
}
|
|
181
184
|
}
|
|
@@ -184,7 +187,9 @@ module.exports = function (RED) {
|
|
|
184
187
|
const regKey = `dmx_fixture_ids_${cfg.siteId}`;
|
|
185
188
|
let reg = {};
|
|
186
189
|
try { reg = globalCtx.get(regKey) || {}; } catch(e) {}
|
|
187
|
-
|
|
190
|
+
const _unreg = reg[fixtureId];
|
|
191
|
+
const _unregNodeId = _unreg && typeof _unreg === 'object' ? _unreg.nodeId : _unreg;
|
|
192
|
+
if (_unregNodeId === node.id) {
|
|
188
193
|
delete reg[fixtureId];
|
|
189
194
|
try { globalCtx.set(regKey, reg); } catch(e) {}
|
|
190
195
|
}
|
|
@@ -349,6 +354,7 @@ module.exports = function (RED) {
|
|
|
349
354
|
|
|
350
355
|
// ── Cleanup ───────────────────────────────────────────────
|
|
351
356
|
node.on('close', function (done) {
|
|
357
|
+
if (S._debugTimer) clearTimeout(S._debugTimer);
|
|
352
358
|
cancelWarmup();
|
|
353
359
|
broker.unsubscribe(S._activePirTopic || S.subscribeTopic, node.id);
|
|
354
360
|
broker.deregister(node, done);
|
package/nodes/ha-mqtt-relay.html
CHANGED
|
@@ -301,7 +301,7 @@
|
|
|
301
301
|
</div>
|
|
302
302
|
|
|
303
303
|
<div style="margin-top:16px; padding-top:8px; border-top:1px solid #444; color:#666; font-size:0.8em; text-align:right;">
|
|
304
|
-
node-red-contrib-dmx-for-ha v0.6.
|
|
304
|
+
node-red-contrib-dmx-for-ha v0.6.33
|
|
305
305
|
</div>
|
|
306
306
|
|
|
307
307
|
</script>
|
package/nodes/ha-mqtt-relay.js
CHANGED
|
@@ -159,7 +159,7 @@ module.exports = function (RED) {
|
|
|
159
159
|
if (S.debugMode) {
|
|
160
160
|
setStatus('red', 'dot', `ha-mqtt-relay "${_debugId}" ⚠ DEBUG MODE ON`);
|
|
161
161
|
node.warn(`[DEBUG] ha-mqtt-relay "${_debugId}" — debug mode is enabled. Disable in production.`);
|
|
162
|
-
setTimeout(function () {
|
|
162
|
+
S._debugTimer = setTimeout(function () {
|
|
163
163
|
if (S.debugMode) {
|
|
164
164
|
S.debugMode = false;
|
|
165
165
|
node.warn(`[DEBUG] ha-mqtt-relay "${_debugId}" — debug mode auto-disabled after 12 hours`);
|
|
@@ -178,11 +178,14 @@ module.exports = function (RED) {
|
|
|
178
178
|
const regKey = `dmx_fixture_ids_${cfg.siteId}`;
|
|
179
179
|
let reg = {};
|
|
180
180
|
try { reg = globalCtx.get(regKey) || {}; } catch(e) {}
|
|
181
|
-
|
|
182
|
-
|
|
181
|
+
// Migration: handle old string format and new object format
|
|
182
|
+
const _existing = reg[fixtureId];
|
|
183
|
+
const _existingNodeId = _existing && typeof _existing === 'object' ? _existing.nodeId : _existing;
|
|
184
|
+
if (_existingNodeId && _existingNodeId !== node.id) {
|
|
185
|
+
node.warn(`${fixtureId} ⚠ DUPLICATE FIXTURE ID — already registered by node $_{_existingNodeId}`);
|
|
183
186
|
setStatus('red', 'dot', `DUPLICATE ID: ${fixtureId}`);
|
|
184
187
|
} else {
|
|
185
|
-
reg[fixtureId] = node.id;
|
|
188
|
+
reg[fixtureId] = { nodeId: node.id, nodeType: 'ha-mqtt-relay' };
|
|
186
189
|
try { globalCtx.set(regKey, reg); } catch(e) {}
|
|
187
190
|
}
|
|
188
191
|
}
|
|
@@ -191,7 +194,9 @@ module.exports = function (RED) {
|
|
|
191
194
|
const regKey = `dmx_fixture_ids_${cfg.siteId}`;
|
|
192
195
|
let reg = {};
|
|
193
196
|
try { reg = globalCtx.get(regKey) || {}; } catch(e) {}
|
|
194
|
-
|
|
197
|
+
const _unreg = reg[fixtureId];
|
|
198
|
+
const _unregNodeId = _unreg && typeof _unreg === 'object' ? _unreg.nodeId : _unreg;
|
|
199
|
+
if (_unregNodeId === node.id) {
|
|
195
200
|
delete reg[fixtureId];
|
|
196
201
|
try { globalCtx.set(regKey, reg); } catch(e) {}
|
|
197
202
|
}
|
|
@@ -453,6 +458,7 @@ module.exports = function (RED) {
|
|
|
453
458
|
|
|
454
459
|
// ── Cleanup ───────────────────────────────────────────────
|
|
455
460
|
node.on('close', function (done) {
|
|
461
|
+
if (S._debugTimer) clearTimeout(S._debugTimer);
|
|
456
462
|
stopEffect();
|
|
457
463
|
if (diskTimer) clearTimeout(diskTimer);
|
|
458
464
|
broker.unsubscribe(cmdTopic, node.id);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "node-red-contrib-dmx-for-ha",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.33",
|
|
4
4
|
"description": "DMX lighting control for Home Assistant via Node-RED and MQTT. Place a node, fill in the settings, deploy. Full HA device registry integration with RGBW/RGBWW/CCT/brightness colour modes, transitions, effects, and group control.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"node-red",
|