node-red-contrib-dmx-for-ha 0.3.4 → 0.3.6

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 CHANGED
@@ -290,6 +290,20 @@ Set to `0` to make all un-timed commands instant.
290
290
 
291
291
  | Version | Changes |
292
292
  |---|---|
293
+ | 0.3.6 | DMX channel conflict detection, jitter timer cancellable on teardown |
294
+ | 0.3.5 | Fix debug mode block initialisation order (S before debug) |
295
+ | 0.3.4 | Debug hint text updated — notes 12hr auto-disable |
296
+ | 0.3.3 | Debug mode canvas warning + 12hr auto-disable safety net |
297
+ | 0.3.2 | Debug output to NR debug tab via node.warn with [DEBUG] prefix |
298
+ | 0.3.1 | Double-fire fix — 5s cooldown + _discovered resets on broker close |
299
+ | 0.3.0 | Debug mode toggle in Advanced section on all nodes |
300
+ | 0.2.9 | _lastSent cache — skip redundant MQTT publishes (unchanged values) |
301
+ | 0.2.8 | Transition jitter, micro-transition threshold, unconfigured ch skip |
302
+ | 0.2.7 | transitionRateLimit + transitionHaUiTime wired from config node |
303
+ | 0.2.6 | Zone optional — omitted from topic when blank |
304
+ | 0.2.5 | buildLocation helper — strips parens, skips duplicates, uses " - " separator |
305
+ | 0.2.4 | sw_version reads from package.json, unique_id = fixtureId only, MW3D removed |
306
+ | 0.2.3 | Canvas label format: L-991-E · Downlight in Bedroom 1 |
293
307
  | 0.2.2 | Group Node status updates, broker feedback, version in editor panel |
294
308
  | 0.2.1 | Hidden mode — entity not auto-placed in dashboard |
295
309
  | 0.2.0 | Canvas button toggles remove/add, auto-discovery on deploy |
@@ -79,21 +79,6 @@ module.exports = function (RED) {
79
79
  node.warn('MQTT broker error: ' + (err.message || err));
80
80
  setStatus('red', 'dot', 'Broker error — check config');
81
81
  });
82
- // ── Debug mode safeguards ─────────────────────────────────────────
83
- if (S.debugMode) {
84
- // Permanent canvas warning so debug mode is obvious
85
- setStatus('red', 'dot', `ha-mqtt-button "${fixtureId}" ⚠ DEBUG MODE ON`);
86
- node.warn(`[DEBUG] ha-mqtt-button "${fixtureId}" — debug mode is enabled. Disable in production.`);
87
-
88
- // Auto-disable after 12 hours — safety net for forgotten debug sessions
89
- setTimeout(function () {
90
- if (S.debugMode) {
91
- S.debugMode = false;
92
- node.warn(`[DEBUG] ha-mqtt-button "${fixtureId}" — debug mode auto-disabled after 12 hours`);
93
- setStatus('yellow', 'ring', `${fixtureId} ready — awaiting HA`);
94
- }
95
- }, 12 * 60 * 60 * 1000);
96
- }
97
82
 
98
83
 
99
84
  // Fallback — if connect event already fired before listener registered
@@ -122,14 +107,18 @@ module.exports = function (RED) {
122
107
  debugMode: config.debugMode === true,
123
108
  };
124
109
 
125
- const fixtureId = `S-${S.uid}${S.uidPostfix}`;
126
- const objectId = `s_${S.uid}${S.uidPostfix}`.toLowerCase().replace(/[^a-z0-9_]/g, '_');
127
- const fixtureTopic = cfg.buildTopic(cfg.discoveryPrefix, 'binary_sensor', fixtureId);
128
- const uiBtnTopic = cfg.buildTopic(cfg.discoveryPrefix, 'button', fixtureId + '-BTN');
129
- const cfgTopic = `${fixtureTopic}/${cfg.configTopic}`;
130
- const statTopic = `${fixtureTopic}/${cfg.stateTopic}`;
131
- const uiBtnCfgTopic = `${uiBtnTopic}/${cfg.configTopic}`;
132
- const uiBtnCmdTopic = `${uiBtnTopic}/${cfg.commandTopic}`;
110
+ // ── Debug mode safeguards ─────────────────────────────────────────
111
+ if (S.debugMode) {
112
+ setStatus('red', 'dot', `ha-mqtt-button "${fixtureId}" ⚠ DEBUG MODE ON`);
113
+ node.warn(`[DEBUG] ha-mqtt-button "${fixtureId}" debug mode is enabled. Disable in production.`);
114
+ setTimeout(function () {
115
+ if (S.debugMode) {
116
+ S.debugMode = false;
117
+ node.warn(`[DEBUG] ha-mqtt-button "${fixtureId}" — debug mode auto-disabled after 12 hours`);
118
+ setStatus('yellow', 'ring', `${fixtureId} ready — awaiting HA`);
119
+ }
120
+ }, 12 * 60 * 60 * 1000);
121
+ }
133
122
 
134
123
  // ── Helpers ───────────────────────────────────────────────
135
124
  function pub(topic, payload, retain) {
@@ -82,21 +82,6 @@ module.exports = function (RED) {
82
82
  node.warn('MQTT broker error: ' + (err.message || err));
83
83
  setStatus('red', 'dot', 'Broker error — check config');
84
84
  });
85
- // ── Debug mode safeguards ─────────────────────────────────────────
86
- if (S.debugMode) {
87
- // Permanent canvas warning so debug mode is obvious
88
- setStatus('red', 'dot', `ha-mqtt-dmx-group "${groupId}" ⚠ DEBUG MODE ON`);
89
- node.warn(`[DEBUG] ha-mqtt-dmx-group "${groupId}" — debug mode is enabled. Disable in production.`);
90
-
91
- // Auto-disable after 12 hours — safety net for forgotten debug sessions
92
- setTimeout(function () {
93
- if (S.debugMode) {
94
- S.debugMode = false;
95
- node.warn(`[DEBUG] ha-mqtt-dmx-group "${groupId}" — debug mode auto-disabled after 12 hours`);
96
- setStatus('yellow', 'ring', `${groupId} ready — awaiting HA`);
97
- }
98
- }, 12 * 60 * 60 * 1000);
99
- }
100
85
 
101
86
 
102
87
  // Fallback — if connect event already fired before listener registered
@@ -129,12 +114,18 @@ module.exports = function (RED) {
129
114
  diskDelay: cfg.diskDelay,
130
115
  };
131
116
 
132
- const groupId = `LG-${S.uid}${S.uidPostfix}`;
133
- const objectId = `lg_${S.uid}${S.uidPostfix}`.toLowerCase().replace(/[^a-z0-9_]/g, '_');
134
- const groupTopic = cfg.buildTopic(cfg.discoveryPrefix, 'light', groupId);
135
- const cfgTopic = `${groupTopic}/${cfg.configTopic}`;
136
- const statTopic = `${groupTopic}/${cfg.stateTopic}`;
137
- const cmdTopic = `${groupTopic}/${cfg.commandTopic}`;
117
+ // ── Debug mode safeguards ─────────────────────────────────────────
118
+ if (S.debugMode) {
119
+ setStatus('red', 'dot', `ha-mqtt-dmx-group "${groupId}" ⚠ DEBUG MODE ON`);
120
+ node.warn(`[DEBUG] ha-mqtt-dmx-group "${groupId}" — debug mode is enabled. Disable in production.`);
121
+ setTimeout(function () {
122
+ if (S.debugMode) {
123
+ S.debugMode = false;
124
+ node.warn(`[DEBUG] ha-mqtt-dmx-group "${groupId}" — debug mode auto-disabled after 12 hours`);
125
+ setStatus('yellow', 'ring', `${groupId} ready — awaiting HA`);
126
+ }
127
+ }, 12 * 60 * 60 * 1000);
128
+ }
138
129
 
139
130
  // ── Context helpers ───────────────────────────────────────
140
131
  // Check disk store available once on startup
@@ -89,21 +89,6 @@ module.exports = function (RED) {
89
89
  node.warn('MQTT broker error: ' + (err.message || err));
90
90
  setStatus('red', 'dot', 'Broker error — check config');
91
91
  });
92
- // ── Debug mode safeguards ─────────────────────────────────────────
93
- if (S.debugMode) {
94
- // Permanent canvas warning so debug mode is obvious
95
- setStatus('red', 'dot', `ha-mqtt-dmx "${fixtureId}" ⚠ DEBUG MODE ON`);
96
- node.warn(`[DEBUG] ha-mqtt-dmx "${fixtureId}" — debug mode is enabled. Disable in production.`);
97
-
98
- // Auto-disable after 12 hours — safety net for forgotten debug sessions
99
- setTimeout(function () {
100
- if (S.debugMode) {
101
- S.debugMode = false;
102
- node.warn(`[DEBUG] ha-mqtt-dmx "${fixtureId}" — debug mode auto-disabled after 12 hours`);
103
- setStatus('yellow', 'ring', `${fixtureId} ready — awaiting HA`);
104
- }
105
- }, 12 * 60 * 60 * 1000);
106
- }
107
92
 
108
93
 
109
94
  // Fallback — if connect event already fired before listener registered
@@ -153,13 +138,18 @@ module.exports = function (RED) {
153
138
  diskDelay: cfg.diskDelay,
154
139
  };
155
140
 
156
- const fixtureId = `${S.uidPrefix}-${S.uid}${S.uidPostfix}`;
157
- const objectId = `${S.uidPrefix}_${S.uid}${S.uidPostfix}`.toLowerCase().replace(/[^a-z0-9_]/g, '_');
158
- const fixtureTopic = cfg.buildTopic(cfg.discoveryPrefix, 'light', fixtureId);
159
- const cfgTopic = `${fixtureTopic}/${cfg.configTopic}`;
160
- const statTopic = `${fixtureTopic}/${cfg.stateTopic}`;
161
- const cmdTopic = `${fixtureTopic}/${cfg.commandTopic}`;
162
- const dmxTopic = cfg.buildTopic(cfg.siteId, cfg.zone, 'dmx', S.universe);
141
+ // ── Debug mode safeguards ─────────────────────────────────────────
142
+ if (S.debugMode) {
143
+ setStatus('red', 'dot', `ha-mqtt-dmx "${fixtureId}" ⚠ DEBUG MODE ON`);
144
+ node.warn(`[DEBUG] ha-mqtt-dmx "${fixtureId}" — debug mode is enabled. Disable in production.`);
145
+ setTimeout(function () {
146
+ if (S.debugMode) {
147
+ S.debugMode = false;
148
+ node.warn(`[DEBUG] ha-mqtt-dmx "${fixtureId}" — debug mode auto-disabled after 12 hours`);
149
+ setStatus('yellow', 'ring', `${fixtureId} ready — awaiting HA`);
150
+ }
151
+ }, 12 * 60 * 60 * 1000);
152
+ }
163
153
 
164
154
  // ── Context helpers ───────────────────────────────────────
165
155
  // Check disk store available once on startup
@@ -253,9 +243,11 @@ module.exports = function (RED) {
253
243
 
254
244
  // ── Effects ───────────────────────────────────────────────
255
245
  let effectTimer = null;
246
+ let jitterTimer = null;
256
247
  let preEffectState = null;
257
248
 
258
249
  function stopEffect() {
250
+ if (jitterTimer) { clearTimeout(jitterTimer); jitterTimer = null; }
259
251
  if (effectTimer) {
260
252
  clearInterval(effectTimer); clearTimeout(effectTimer);
261
253
  effectTimer = null;
@@ -339,7 +331,7 @@ module.exports = function (RED) {
339
331
  // Prevents thundering herd when many nodes start transitions simultaneously
340
332
  const jitter = Math.random() * intervalMs;
341
333
  // Fix 3: Jitter delay spreads load across event loop
342
- setTimeout(function () {
334
+ jitterTimer = setTimeout(function () {
343
335
  effectTimer = setInterval(function () {
344
336
  tick++;
345
337
  const progress = Math.min(1, tick / totalTicks);
@@ -531,6 +523,48 @@ module.exports = function (RED) {
531
523
  return [[vv,t,p],[q,vv,p],[p,vv,t],[p,q,vv],[t,p,vv],[vv,p,q]][i];
532
524
  }
533
525
 
526
+
527
+ // ── DMX channel conflict detection ───────────────────────────────────
528
+ function checkChannelConflicts() {
529
+ const globalCtx = node.context().global;
530
+ const registryKey = `dmx_channels_${cfg.siteId}_${cfg.zone}_${S.universe}`;
531
+ let registry = {};
532
+ try { registry = globalCtx.get(registryKey) || {}; } catch(e) {}
533
+
534
+ const myChannels = [S.ch.red, S.ch.green, S.ch.blue, S.ch.white, S.ch.warmWhite]
535
+ .filter(ch => ch && ch > 0);
536
+
537
+ let conflicts = [];
538
+ myChannels.forEach(function(ch) {
539
+ if (registry[ch] && registry[ch] !== fixtureId) {
540
+ conflicts.push(`ch${ch} already used by ${registry[ch]}`);
541
+ }
542
+ registry[ch] = fixtureId;
543
+ });
544
+
545
+ try { globalCtx.set(registryKey, registry); } catch(e) {}
546
+
547
+ if (conflicts.length > 0) {
548
+ const msg = `${fixtureId} — DMX CHANNEL CONFLICT: ${conflicts.join(', ')}`;
549
+ node.warn(msg);
550
+ setStatus('red', 'dot', `${fixtureId} ⚠ CHANNEL CONFLICT`);
551
+ return false;
552
+ }
553
+ return true;
554
+ }
555
+
556
+ function clearChannelRegistry() {
557
+ try {
558
+ const globalCtx = node.context().global;
559
+ const registryKey = `dmx_channels_${cfg.siteId}_${cfg.zone}_${S.universe}`;
560
+ const registry = globalCtx.get(registryKey) || {};
561
+ [S.ch.red, S.ch.green, S.ch.blue, S.ch.white, S.ch.warmWhite]
562
+ .filter(ch => ch && ch > 0)
563
+ .forEach(ch => { if (registry[ch] === fixtureId) delete registry[ch]; });
564
+ globalCtx.set(registryKey, registry);
565
+ } catch(e) {}
566
+ }
567
+
534
568
  // ── Device add ────────────────────────────────────────────
535
569
  function handleDeviceAdd() {
536
570
  if (ctxGet('state') === undefined && ctxGet('state', 'disk_values') === undefined) {
@@ -575,6 +609,8 @@ module.exports = function (RED) {
575
609
  };
576
610
 
577
611
  pub(cfgTopic, discovery, true);
612
+ // Check for DMX channel conflicts before discovery
613
+ if (!checkChannelConflicts()) return;
578
614
  resetLastSent(); // Force full state re-publish after discovery
579
615
 
580
616
  broker.subscribe(cmdTopic, cfg.qos, function (topic, rawPayload) {
@@ -617,6 +653,7 @@ module.exports = function (RED) {
617
653
 
618
654
  // ── Device remove ─────────────────────────────────────────
619
655
  function handleDeviceRemove() {
656
+ clearChannelRegistry();
620
657
  stopEffect();
621
658
  if (diskTimer) { clearTimeout(diskTimer); diskTimer = null; }
622
659
  ['state','brightness','red','green','blue','white','warmWhite'].forEach(function (k) {
@@ -666,6 +703,7 @@ module.exports = function (RED) {
666
703
 
667
704
  // ── Cleanup ───────────────────────────────────────────────
668
705
  node.on('close', function (done) {
706
+ clearChannelRegistry();
669
707
  stopEffect();
670
708
  if (diskTimer) clearTimeout(diskTimer);
671
709
  broker.unsubscribe(cmdTopic, node.id);
@@ -79,21 +79,6 @@ module.exports = function (RED) {
79
79
  node.warn('MQTT broker error: ' + (err.message || err));
80
80
  setStatus('red', 'dot', 'Broker error — check config');
81
81
  });
82
- // ── Debug mode safeguards ─────────────────────────────────────────
83
- if (S.debugMode) {
84
- // Permanent canvas warning so debug mode is obvious
85
- setStatus('red', 'dot', `ha-mqtt-pir "${fixtureId}" ⚠ DEBUG MODE ON`);
86
- node.warn(`[DEBUG] ha-mqtt-pir "${fixtureId}" — debug mode is enabled. Disable in production.`);
87
-
88
- // Auto-disable after 12 hours — safety net for forgotten debug sessions
89
- setTimeout(function () {
90
- if (S.debugMode) {
91
- S.debugMode = false;
92
- node.warn(`[DEBUG] ha-mqtt-pir "${fixtureId}" — debug mode auto-disabled after 12 hours`);
93
- setStatus('yellow', 'ring', `${fixtureId} ready — awaiting HA`);
94
- }
95
- }, 12 * 60 * 60 * 1000);
96
- }
97
82
 
98
83
 
99
84
  // Fallback — if connect event already fired before listener registered
@@ -123,13 +108,18 @@ module.exports = function (RED) {
123
108
  debugMode: config.debugMode === true,
124
109
  };
125
110
 
126
- const fixtureId = `S-${S.uid}${S.uidPostfix}`;
127
- const objectId = `s_${S.uid}${S.uidPostfix}`.toLowerCase().replace(/[^a-z0-9_]/g, '_');
128
- const fixtureTopic = cfg.buildTopic(cfg.discoveryPrefix, 'binary_sensor', fixtureId);
129
- const cmdTopic = `${fixtureTopic}/${cfg.commandTopic}`;
130
- const cfgTopic = `${fixtureTopic}/${cfg.configTopic}`;
131
- const statTopic = `${fixtureTopic}/${cfg.stateTopic}`;
132
- const avtyTopic = `${fixtureTopic}/${cfg.availTopic}`;
111
+ // ── Debug mode safeguards ─────────────────────────────────────────
112
+ if (S.debugMode) {
113
+ setStatus('red', 'dot', `ha-mqtt-pir "${fixtureId}" ⚠ DEBUG MODE ON`);
114
+ node.warn(`[DEBUG] ha-mqtt-pir "${fixtureId}" — debug mode is enabled. Disable in production.`);
115
+ setTimeout(function () {
116
+ if (S.debugMode) {
117
+ S.debugMode = false;
118
+ node.warn(`[DEBUG] ha-mqtt-pir "${fixtureId}" — debug mode auto-disabled after 12 hours`);
119
+ setStatus('yellow', 'ring', `${fixtureId} ready — awaiting HA`);
120
+ }
121
+ }, 12 * 60 * 60 * 1000);
122
+ }
133
123
 
134
124
  // ── Helpers ───────────────────────────────────────────────
135
125
  function pub(topic, payload, retain) {
@@ -82,21 +82,6 @@ module.exports = function (RED) {
82
82
  node.warn('MQTT broker error: ' + (err.message || err));
83
83
  setStatus('red', 'dot', 'Broker error — check config');
84
84
  });
85
- // ── Debug mode safeguards ─────────────────────────────────────────
86
- if (S.debugMode) {
87
- // Permanent canvas warning so debug mode is obvious
88
- setStatus('red', 'dot', `ha-mqtt-relay "${fixtureId}" ⚠ DEBUG MODE ON`);
89
- node.warn(`[DEBUG] ha-mqtt-relay "${fixtureId}" — debug mode is enabled. Disable in production.`);
90
-
91
- // Auto-disable after 12 hours — safety net for forgotten debug sessions
92
- setTimeout(function () {
93
- if (S.debugMode) {
94
- S.debugMode = false;
95
- node.warn(`[DEBUG] ha-mqtt-relay "${fixtureId}" — debug mode auto-disabled after 12 hours`);
96
- setStatus('yellow', 'ring', `${fixtureId} ready — awaiting HA`);
97
- }
98
- }, 12 * 60 * 60 * 1000);
99
- }
100
85
 
101
86
 
102
87
  // Fallback — if connect event already fired before listener registered
@@ -129,13 +114,18 @@ module.exports = function (RED) {
129
114
  diskDelay: cfg.diskDelay,
130
115
  };
131
116
 
132
- const fixtureId = `${S.uidPrefix}-${S.uid}${S.uidPostfix}`;
133
- const objectId = `${S.uidPrefix}_${S.uid}${S.uidPostfix}`.toLowerCase().replace(/[^a-z0-9_]/g, '_');
134
- const fixtureTopic = cfg.buildTopic(cfg.discoveryPrefix, 'light', fixtureId);
135
- const cfgTopic = `${fixtureTopic}/${cfg.configTopic}`;
136
- const statTopic = `${fixtureTopic}/${cfg.stateTopic}`;
137
- const cmdTopic = `${fixtureTopic}/${cfg.commandTopic}`;
138
- const relayTopic = cfg.buildTopic(cfg.siteId, cfg.zone, S.controllerNum, S.mqttSegment, S.relayNum);
117
+ // ── Debug mode safeguards ─────────────────────────────────────────
118
+ if (S.debugMode) {
119
+ setStatus('red', 'dot', `ha-mqtt-relay "${fixtureId}" ⚠ DEBUG MODE ON`);
120
+ node.warn(`[DEBUG] ha-mqtt-relay "${fixtureId}" — debug mode is enabled. Disable in production.`);
121
+ setTimeout(function () {
122
+ if (S.debugMode) {
123
+ S.debugMode = false;
124
+ node.warn(`[DEBUG] ha-mqtt-relay "${fixtureId}" — debug mode auto-disabled after 12 hours`);
125
+ setStatus('yellow', 'ring', `${fixtureId} ready — awaiting HA`);
126
+ }
127
+ }, 12 * 60 * 60 * 1000);
128
+ }
139
129
 
140
130
  // ── Context helpers ───────────────────────────────────────
141
131
  // Check disk store available once on startup
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-red-contrib-dmx-for-ha",
3
- "version": "0.3.4",
3
+ "version": "0.3.6",
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",