node-red-contrib-dmx-for-ha 0.3.7 → 0.3.9

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.
@@ -56,7 +56,7 @@ module.exports = function (RED) {
56
56
  // but still respect cooldown to prevent accidental rapid-fire
57
57
  const now = Date.now();
58
58
  if (now - _lastDiscoveryTime < _DISCOVERY_COOLDOWN_MS) {
59
- node.warn('Discovery cooldown active — please wait 5 seconds between manual triggers');
59
+ if (S.debugMode) node.debug('Discovery cooldown active — skipping duplicate auto-discover');
60
60
  return;
61
61
  }
62
62
  _lastDiscoveryTime = now;
@@ -121,6 +121,17 @@ module.exports = function (RED) {
121
121
  }, 12 * 60 * 60 * 1000);
122
122
  }
123
123
 
124
+
125
+ // ── Fixture identity and topics ───────────────────────────────────────
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 uiBtnTopic = cfg.buildTopic(cfg.discoveryPrefix, 'button', fixtureId + '-BTN');
130
+ const cfgTopic = `${fixtureTopic}/${cfg.configTopic}`;
131
+ const statTopic = `${fixtureTopic}/${cfg.stateTopic}`;
132
+ const uiBtnCfgTopic = `${uiBtnTopic}/${cfg.configTopic}`;
133
+ const uiBtnCmdTopic = `${uiBtnTopic}/${cfg.commandTopic}`;
134
+
124
135
  // ── Helpers ───────────────────────────────────────────────
125
136
  function pub(topic, payload, retain) {
126
137
  const strPayload = typeof payload === 'object' ? JSON.stringify(payload) : String(payload);
@@ -59,7 +59,7 @@ module.exports = function (RED) {
59
59
  // but still respect cooldown to prevent accidental rapid-fire
60
60
  const now = Date.now();
61
61
  if (now - _lastDiscoveryTime < _DISCOVERY_COOLDOWN_MS) {
62
- node.warn('Discovery cooldown active — please wait 5 seconds between manual triggers');
62
+ if (S.debugMode) node.debug('Discovery cooldown active — skipping duplicate auto-discover');
63
63
  return;
64
64
  }
65
65
  _lastDiscoveryTime = now;
@@ -128,6 +128,15 @@ module.exports = function (RED) {
128
128
  }, 12 * 60 * 60 * 1000);
129
129
  }
130
130
 
131
+
132
+ // ── Group identity and topics ─────────────────────────────────────────
133
+ const groupId = `LG-${S.uid}${S.uidPostfix}`;
134
+ const objectId = `lg_${S.uid}${S.uidPostfix}`.toLowerCase().replace(/[^a-z0-9_]/g, '_');
135
+ const groupTopic = cfg.buildTopic(cfg.discoveryPrefix, 'light', groupId);
136
+ const cfgTopic = `${groupTopic}/${cfg.configTopic}`;
137
+ const statTopic = `${groupTopic}/${cfg.stateTopic}`;
138
+ const cmdTopic = `${groupTopic}/${cfg.commandTopic}`;
139
+
131
140
  // ── Context helpers ───────────────────────────────────────
132
141
  // Check disk store available once on startup
133
142
  let diskAvailable = false;
@@ -276,14 +285,11 @@ module.exports = function (RED) {
276
285
  forwardToChildren(payload, null);
277
286
  const _lbl = payload.effect ? `effect:${payload.effect}` : `${payload.state}`;
278
287
  setStatus('green', 'dot', `${groupId} → ${_lbl}`);
279
- setTimeout(() => setStatus('yellow', 'ring', `${groupId} ready`), 3000);
280
288
  }, node.id);
281
289
 
282
290
  setStatus('green', 'ring', `${groupId} discovery sent`);
283
291
  node.log(`Group device added: "${S.groupName || groupId}"`);
284
292
 
285
- // Forward device:add to children via Link so they self-discover
286
- node.send([{ device: 'add' }]);
287
293
 
288
294
  // Recovery
289
295
  setTimeout(() => {
@@ -322,7 +328,6 @@ module.exports = function (RED) {
322
328
  pubState({ state: msg.payload.state, color_mode: S.colorMode, brightness: msg.payload.brightness });
323
329
  forwardToChildren(msg.payload, msg.dmx_trace);
324
330
  setStatus('green', 'dot', `${groupId} → cascade:${msg.payload.state}`);
325
- setTimeout(() => setStatus('yellow', 'ring', `${groupId} ready`), 3000);
326
331
  }
327
332
  } else {
328
333
  const devReq = typeof msg.device === 'string'
@@ -340,7 +345,6 @@ module.exports = function (RED) {
340
345
  pubState({ state: msg.payload.state, color_mode: S.colorMode, brightness: msg.payload.brightness });
341
346
  forwardToChildren(msg.payload, null);
342
347
  setStatus('green', 'dot', `${groupId} → ${msg.payload.state}`);
343
- setTimeout(() => setStatus('yellow', 'ring', `${groupId} ready`), 3000);
344
348
  } else {
345
349
  node.warn(`${groupId} — unrecognised message received and dropped. See node documentation.`);
346
350
  }
@@ -66,7 +66,7 @@ module.exports = function (RED) {
66
66
  // but still respect cooldown to prevent accidental rapid-fire
67
67
  const now = Date.now();
68
68
  if (now - _lastDiscoveryTime < _DISCOVERY_COOLDOWN_MS) {
69
- node.warn('Discovery cooldown active — please wait 5 seconds between manual triggers');
69
+ if (S.debugMode) node.debug('Discovery cooldown active — skipping duplicate auto-discover');
70
70
  return;
71
71
  }
72
72
  _lastDiscoveryTime = now;
@@ -176,7 +176,20 @@ module.exports = function (RED) {
176
176
  // ── Disk save timer ───────────────────────────────────────
177
177
  let diskTimer = null;
178
178
  function startDiskSave(onComplete) {
179
- if (diskTimer) { clearTimeout(diskTimer); diskTimer = null; }
179
+ if (diskTimer) {
180
+ clearTimeout(diskTimer);
181
+ diskTimer = null;
182
+ // Flush pending disk save immediately on close
183
+ const s=ctxGet('state'); const br=ctxGet('brightness');
184
+ const r=ctxGet('red'); const g=ctxGet('green'); const b=ctxGet('blue');
185
+ const w=ctxGet('white'); const ww=ctxGet('warmWhite');
186
+ if (s !== undefined) {
187
+ ctxSet('state',s,'disk_values'); ctxSet('brightness',br,'disk_values');
188
+ ctxSet('red',r,'disk_values'); ctxSet('green',g,'disk_values');
189
+ ctxSet('blue',b,'disk_values'); ctxSet('white',w,'disk_values');
190
+ ctxSet('warmWhite',ww,'disk_values');
191
+ }
192
+ }
180
193
  diskTimer = setTimeout(() => { diskTimer = null; onComplete(); }, S.diskDelay * 1000);
181
194
  }
182
195
 
@@ -525,6 +538,16 @@ module.exports = function (RED) {
525
538
  }
526
539
 
527
540
 
541
+
542
+ // ── Fixture identity and topics ───────────────────────────────────────
543
+ const fixtureId = `${S.uidPrefix}-${S.uid}${S.uidPostfix}`;
544
+ const objectId = `${S.uidPrefix}_${S.uid}${S.uidPostfix}`.toLowerCase().replace(/[^a-z0-9_]/g, '_');
545
+ const fixtureTopic = cfg.buildTopic(cfg.discoveryPrefix, 'light', fixtureId);
546
+ const cfgTopic = `${fixtureTopic}/${cfg.configTopic}`;
547
+ const statTopic = `${fixtureTopic}/${cfg.stateTopic}`;
548
+ const cmdTopic = `${fixtureTopic}/${cfg.commandTopic}`;
549
+ const dmxTopic = cfg.buildTopic(cfg.siteId, cfg.zone, 'dmx', S.universe);
550
+
528
551
  // ── DMX channel conflict detection ───────────────────────────────────
529
552
  function checkChannelConflicts() {
530
553
  const globalCtx = node.context().global;
@@ -656,7 +679,20 @@ module.exports = function (RED) {
656
679
  function handleDeviceRemove() {
657
680
  clearChannelRegistry();
658
681
  stopEffect();
659
- if (diskTimer) { clearTimeout(diskTimer); diskTimer = null; }
682
+ if (diskTimer) {
683
+ clearTimeout(diskTimer);
684
+ diskTimer = null;
685
+ // Flush pending disk save immediately on close
686
+ const s=ctxGet('state'); const br=ctxGet('brightness');
687
+ const r=ctxGet('red'); const g=ctxGet('green'); const b=ctxGet('blue');
688
+ const w=ctxGet('white'); const ww=ctxGet('warmWhite');
689
+ if (s !== undefined) {
690
+ ctxSet('state',s,'disk_values'); ctxSet('brightness',br,'disk_values');
691
+ ctxSet('red',r,'disk_values'); ctxSet('green',g,'disk_values');
692
+ ctxSet('blue',b,'disk_values'); ctxSet('white',w,'disk_values');
693
+ ctxSet('warmWhite',ww,'disk_values');
694
+ }
695
+ }
660
696
  ['state','brightness','red','green','blue','white','warmWhite'].forEach(function (k) {
661
697
  ctxSet(k, null); ctxSet(k, null, 'disk_values');
662
698
  });
@@ -56,7 +56,7 @@ module.exports = function (RED) {
56
56
  // but still respect cooldown to prevent accidental rapid-fire
57
57
  const now = Date.now();
58
58
  if (now - _lastDiscoveryTime < _DISCOVERY_COOLDOWN_MS) {
59
- node.warn('Discovery cooldown active — please wait 5 seconds between manual triggers');
59
+ if (S.debugMode) node.debug('Discovery cooldown active — skipping duplicate auto-discover');
60
60
  return;
61
61
  }
62
62
  _lastDiscoveryTime = now;
@@ -122,6 +122,16 @@ module.exports = function (RED) {
122
122
  }, 12 * 60 * 60 * 1000);
123
123
  }
124
124
 
125
+
126
+ // ── Fixture identity and topics ───────────────────────────────────────
127
+ const fixtureId = `S-${S.uid}${S.uidPostfix}`;
128
+ const objectId = `s_${S.uid}${S.uidPostfix}`.toLowerCase().replace(/[^a-z0-9_]/g, '_');
129
+ const fixtureTopic = cfg.buildTopic(cfg.discoveryPrefix, 'binary_sensor', fixtureId);
130
+ const cfgTopic = `${fixtureTopic}/${cfg.configTopic}`;
131
+ const statTopic = `${fixtureTopic}/${cfg.stateTopic}`;
132
+ const cmdTopic = `${fixtureTopic}/${cfg.commandTopic}`;
133
+ const avtyTopic = `${fixtureTopic}/${cfg.availTopic}`;
134
+
125
135
  // ── Helpers ───────────────────────────────────────────────
126
136
  function pub(topic, payload, retain) {
127
137
  const strPayload = typeof payload === 'object' ? JSON.stringify(payload) : String(payload);
@@ -59,7 +59,7 @@ module.exports = function (RED) {
59
59
  // but still respect cooldown to prevent accidental rapid-fire
60
60
  const now = Date.now();
61
61
  if (now - _lastDiscoveryTime < _DISCOVERY_COOLDOWN_MS) {
62
- node.warn('Discovery cooldown active — please wait 5 seconds between manual triggers');
62
+ if (S.debugMode) node.debug('Discovery cooldown active — skipping duplicate auto-discover');
63
63
  return;
64
64
  }
65
65
  _lastDiscoveryTime = now;
@@ -128,6 +128,16 @@ module.exports = function (RED) {
128
128
  }, 12 * 60 * 60 * 1000);
129
129
  }
130
130
 
131
+
132
+ // ── Fixture identity and topics ───────────────────────────────────────
133
+ const fixtureId = `${S.uidPrefix}-${S.uid}${S.uidPostfix}`;
134
+ const objectId = `${S.uidPrefix}_${S.uid}${S.uidPostfix}`.toLowerCase().replace(/[^a-z0-9_]/g, '_');
135
+ const fixtureTopic = cfg.buildTopic(cfg.discoveryPrefix, 'light', fixtureId);
136
+ const cfgTopic = `${fixtureTopic}/${cfg.configTopic}`;
137
+ const statTopic = `${fixtureTopic}/${cfg.stateTopic}`;
138
+ const cmdTopic = `${fixtureTopic}/${cfg.commandTopic}`;
139
+ const relayTopic = cfg.buildTopic(cfg.siteId, cfg.zone, S.controllerNum, S.mqttSegment, S.relayNum);
140
+
131
141
  // ── Context helpers ───────────────────────────────────────
132
142
  // Check disk store available once on startup
133
143
  let diskAvailable = false;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-red-contrib-dmx-for-ha",
3
- "version": "0.3.7",
3
+ "version": "0.3.9",
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",