node-red-contrib-dmx-for-ha 0.5.2 → 0.6.2
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/nodes/ha-mqtt-button.html +28 -4
- package/nodes/ha-mqtt-button.js +58 -7
- package/nodes/ha-mqtt-config.html +3 -3
- package/nodes/ha-mqtt-dmx-group.html +1 -1
- package/nodes/ha-mqtt-dmx-group.js +26 -0
- package/nodes/ha-mqtt-dmx.html +34 -14
- package/nodes/ha-mqtt-dmx.js +97 -23
- package/nodes/ha-mqtt-pir.html +30 -6
- package/nodes/ha-mqtt-pir.js +72 -9
- package/nodes/ha-mqtt-relay.html +15 -4
- package/nodes/ha-mqtt-relay.js +54 -4
- package/package.json +16 -9
- package/profiles/README.md +105 -0
- package/profiles/button/custom.js +17 -0
- package/profiles/button/mw3d_v1.js +37 -0
- package/profiles/button/mw3d_v2.js +36 -0
- package/profiles/dmx/custom.js +65 -0
- package/profiles/dmx/etherten_v1.js +26 -0
- package/profiles/dmx/etherten_v2.js +32 -0
- package/profiles/dmx/mqtt_json.js +27 -0
- package/profiles/index.js +45 -0
- package/profiles/pir/custom.js +15 -0
- package/profiles/pir/mw3d_v1.js +34 -0
- package/profiles/pir/mw3d_v2.js +38 -0
- package/profiles/relay/bedrock_v1.js +17 -0
- package/profiles/relay/custom.js +26 -0
- package/profiles/relay/generic_onoff.js +16 -0
- package/profiles/relay/mqtt_json.js +15 -0
- package/docs/config_node_spec.md +0 -236
- package/docs/dmx_node_env_reference.md +0 -341
- package/docs/master_todo.md +0 -428
- package/docs/node_contracts.md +0 -278
- package/docs/nr_subflow_gotchas.md +0 -258
- package/node-red-contrib-dmx-for-ha/LICENSE +0 -28
- package/node-red-contrib-dmx-for-ha/README.md +0 -587
- package/node-red-contrib-dmx-for-ha/docs/config_node_spec.md +0 -236
- package/node-red-contrib-dmx-for-ha/docs/dmx_node_env_reference.md +0 -341
- package/node-red-contrib-dmx-for-ha/docs/master_todo.md +0 -428
- package/node-red-contrib-dmx-for-ha/docs/node_contracts.md +0 -278
- package/node-red-contrib-dmx-for-ha/docs/nr_subflow_gotchas.md +0 -258
- package/node-red-contrib-dmx-for-ha/nodes/ha-mqtt-button.html +0 -326
- package/node-red-contrib-dmx-for-ha/nodes/ha-mqtt-button.js +0 -284
- package/node-red-contrib-dmx-for-ha/nodes/ha-mqtt-config.html +0 -270
- package/node-red-contrib-dmx-for-ha/nodes/ha-mqtt-config.js +0 -99
- package/node-red-contrib-dmx-for-ha/nodes/ha-mqtt-dmx-group.html +0 -387
- package/node-red-contrib-dmx-for-ha/nodes/ha-mqtt-dmx-group.js +0 -410
- package/node-red-contrib-dmx-for-ha/nodes/ha-mqtt-dmx.html +0 -618
- package/node-red-contrib-dmx-for-ha/nodes/ha-mqtt-dmx.js +0 -808
- package/node-red-contrib-dmx-for-ha/nodes/ha-mqtt-pir.html +0 -337
- package/node-red-contrib-dmx-for-ha/nodes/ha-mqtt-pir.js +0 -306
- package/node-red-contrib-dmx-for-ha/nodes/ha-mqtt-relay.html +0 -329
- package/node-red-contrib-dmx-for-ha/nodes/ha-mqtt-relay.js +0 -424
- package/node-red-contrib-dmx-for-ha/package.json +0 -39
- package/node-red-contrib-dmx-for-ha/subflow/README.md +0 -35
- package/node-red-contrib-dmx-for-ha/subflow/button_node_v5.0.3.js +0 -324
- package/node-red-contrib-dmx-for-ha/subflow/dmx_group_node_v0.3.8.js +0 -860
- package/node-red-contrib-dmx-for-ha/subflow/dmx_node_v0.5.9.js +0 -1994
- package/node-red-contrib-dmx-for-ha/subflow/pir_node_v1.0.3.js +0 -365
- package/node-red-contrib-dmx-for-ha/subflow/relay_node_v4.0.2.js +0 -553
- package/node-red-contrib-dmx-for-ha/subflow/subflow_definitions.json +0 -6154
- package/subflow/README.md +0 -35
- package/subflow/button_node_v5.0.3.js +0 -324
- package/subflow/dmx_group_node_v0.3.8.js +0 -860
- package/subflow/dmx_node_v0.5.9.js +0 -1994
- package/subflow/pir_node_v1.0.3.js +0 -365
- package/subflow/relay_node_v4.0.2.js +0 -553
- package/subflow/subflow_definitions.json +0 -6154
|
@@ -41,8 +41,9 @@
|
|
|
41
41
|
subLocation: { value: '' },
|
|
42
42
|
// Button specifics
|
|
43
43
|
buttonPosition: { value: '' },
|
|
44
|
-
buttonPayload:
|
|
45
|
-
subscribeTopic:
|
|
44
|
+
buttonPayload: { value: '', required: true },
|
|
45
|
+
subscribeTopic: { value: '', required: true },
|
|
46
|
+
controllerProfile: { value: 'mw3d_v1' },
|
|
46
47
|
// Options
|
|
47
48
|
haIcon: { value: 'mdi:gesture-tap-button' },
|
|
48
49
|
ledColor: { value: 'Blue' },
|
|
@@ -239,12 +240,35 @@
|
|
|
239
240
|
</div>
|
|
240
241
|
|
|
241
242
|
<div class="form-row">
|
|
243
|
+
<label for="node-input-controllerProfile"><i class="fa fa-microchip"></i> Profile</label>
|
|
244
|
+
<select id="node-input-controllerProfile" style="width:65%" onchange="
|
|
245
|
+
var v = this.value;
|
|
246
|
+
document.getElementById('btn-payload-row').style.display = (v === 'mw3d_v1' || v === 'custom') ? '' : 'none';
|
|
247
|
+
document.getElementById('btn-topic-row').style.display = (v === 'mw3d_v1' || v === 'custom') ? '' : 'none';
|
|
248
|
+
document.getElementById('btn-v2-note').style.display = (v === 'mw3d_v2') ? '' : 'none';
|
|
249
|
+
">
|
|
250
|
+
<option value="mw3d_v1">MW3D v1 — Legacy (panelId-GPIO payload)</option>
|
|
251
|
+
<option value="mw3d_v2">MW3D v2 — JSON (fixture ID payload)</option>
|
|
252
|
+
<option value="custom">Custom</option>
|
|
253
|
+
</select>
|
|
254
|
+
</div>
|
|
255
|
+
|
|
256
|
+
<div class="form-row" id="btn-v2-note" style="display:none">
|
|
257
|
+
<label></label>
|
|
258
|
+
<span style="color:#999; font-size:0.85em;">
|
|
259
|
+
<i class="fa fa-info-circle"></i>
|
|
260
|
+
v2 firmware — topic and payload auto-configured from zone + fixture ID.
|
|
261
|
+
No manual payload or topic needed.
|
|
262
|
+
</span>
|
|
263
|
+
</div>
|
|
264
|
+
|
|
265
|
+
<div class="form-row" id="btn-payload-row">
|
|
242
266
|
<label for="node-input-buttonPayload"><i class="fa fa-hand-pointer-o"></i> Payload</label>
|
|
243
267
|
<input type="text" id="node-input-buttonPayload" placeholder="Required — e.g. 10-54" style="width:120px" />
|
|
244
268
|
<span style="margin-left:8px; color:#999; font-size:0.85em;">panelId-GPIOpin</span>
|
|
245
269
|
</div>
|
|
246
270
|
|
|
247
|
-
<div class="form-row">
|
|
271
|
+
<div class="form-row" id="btn-topic-row">
|
|
248
272
|
<label for="node-input-subscribeTopic"><i class="fa fa-exchange"></i> Subscribe topic</label>
|
|
249
273
|
<input type="text" id="node-input-subscribeTopic" placeholder="Required — e.g. buttons or home/zone/buttons" style="width:55%" />
|
|
250
274
|
</div>
|
|
@@ -289,7 +313,7 @@
|
|
|
289
313
|
</div>
|
|
290
314
|
|
|
291
315
|
<div style="margin-top:16px; padding-top:8px; border-top:1px solid #444; color:#666; font-size:0.8em; text-align:right;">
|
|
292
|
-
node-red-contrib-dmx-for-ha v0.
|
|
316
|
+
node-red-contrib-dmx-for-ha v0.6.1
|
|
293
317
|
</div>
|
|
294
318
|
|
|
295
319
|
</script>
|
package/nodes/ha-mqtt-button.js
CHANGED
|
@@ -106,8 +106,9 @@ module.exports = function (RED) {
|
|
|
106
106
|
situation: config.situation || 'in',
|
|
107
107
|
subLocation: config.subLocation || '',
|
|
108
108
|
buttonPosition: config.buttonPosition || 'Single',
|
|
109
|
-
buttonPayload:
|
|
110
|
-
subscribeTopic:
|
|
109
|
+
buttonPayload: String(config.buttonPayload || ''),
|
|
110
|
+
subscribeTopic: config.subscribeTopic || 'buttons',
|
|
111
|
+
controllerProfile: config.controllerProfile || 'mw3d_v1',
|
|
111
112
|
haIcon: config.haIcon || 'mdi:gesture-tap-button',
|
|
112
113
|
ledColor: config.ledColor || 'Blue',
|
|
113
114
|
holdTime: parseFloat(config.holdTime) || 0.5,
|
|
@@ -129,8 +130,49 @@ module.exports = function (RED) {
|
|
|
129
130
|
}
|
|
130
131
|
|
|
131
132
|
|
|
133
|
+
// ── Controller profile ────────────────────────────────────────────
|
|
134
|
+
let _btnProfile;
|
|
135
|
+
try {
|
|
136
|
+
const _profiles = require('../profiles/index');
|
|
137
|
+
_btnProfile = (_profiles.button || []).find(p => p.id === S.controllerProfile)
|
|
138
|
+
|| _profiles.button[0];
|
|
139
|
+
} catch(e) {
|
|
140
|
+
_btnProfile = {
|
|
141
|
+
id: 'mw3d_v1',
|
|
142
|
+
buildTopic: () => null,
|
|
143
|
+
parsePayload: (raw, fid, exp) => raw === exp ? { match: true, state: 'on' } : null,
|
|
144
|
+
hasOffEvent: false
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
const btnProfile = _btnProfile;
|
|
148
|
+
|
|
132
149
|
// ── Fixture identity and topics ───────────────────────────────────────
|
|
133
150
|
const fixtureId = `S-${S.uid}${S.uidPostfix}`;
|
|
151
|
+
// ── Fixture ID duplicate detection ──────────────────────────────────
|
|
152
|
+
function registerFixtureId() {
|
|
153
|
+
const globalCtx = node.context().global;
|
|
154
|
+
const regKey = `dmx_fixture_ids_${cfg.siteId}`;
|
|
155
|
+
let reg = {};
|
|
156
|
+
try { reg = globalCtx.get(regKey) || {}; } catch(e) {}
|
|
157
|
+
if (reg[fixtureId] && reg[fixtureId] !== node.id) {
|
|
158
|
+
node.warn(`${fixtureId} ⚠ DUPLICATE FIXTURE ID — already registered by node ${reg[fixtureId]}`);
|
|
159
|
+
setStatus('red', 'dot', `DUPLICATE ID: ${fixtureId}`);
|
|
160
|
+
} else {
|
|
161
|
+
reg[fixtureId] = node.id;
|
|
162
|
+
try { globalCtx.set(regKey, reg); } catch(e) {}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
function unregisterFixtureId() {
|
|
166
|
+
const globalCtx = node.context().global;
|
|
167
|
+
const regKey = `dmx_fixture_ids_${cfg.siteId}`;
|
|
168
|
+
let reg = {};
|
|
169
|
+
try { reg = globalCtx.get(regKey) || {}; } catch(e) {}
|
|
170
|
+
if (reg[fixtureId] === node.id) {
|
|
171
|
+
delete reg[fixtureId];
|
|
172
|
+
try { globalCtx.set(regKey, reg); } catch(e) {}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
134
176
|
const objectId = `s_${S.uid}${S.uidPostfix}`.toLowerCase().replace(/[^a-z0-9_]/g, '_');
|
|
135
177
|
const fixtureTopic = cfg.buildTopic(cfg.discoveryPrefix, 'binary_sensor', fixtureId);
|
|
136
178
|
const uiBtnTopic = cfg.buildTopic(cfg.discoveryPrefix, 'button', fixtureId + '-BTN');
|
|
@@ -164,6 +206,7 @@ module.exports = function (RED) {
|
|
|
164
206
|
|
|
165
207
|
// ── Device add ────────────────────────────────────────────
|
|
166
208
|
function handleDeviceAdd() {
|
|
209
|
+
registerFixtureId();
|
|
167
210
|
// Binary sensor discovery
|
|
168
211
|
const bsDiscovery = {
|
|
169
212
|
unique_id: fixtureId,
|
|
@@ -201,9 +244,16 @@ module.exports = function (RED) {
|
|
|
201
244
|
pub(cfgTopic, bsDiscovery, true);
|
|
202
245
|
pub(uiBtnCfgTopic, uiDiscovery, cfg.retainDiscovery);
|
|
203
246
|
|
|
204
|
-
// Subscribe to controller topic
|
|
205
|
-
|
|
206
|
-
|
|
247
|
+
// Subscribe to controller topic — topic and matching via profile
|
|
248
|
+
const _btnSubTopic = btnProfile.buildTopic ? btnProfile.buildTopic(cfg) : null;
|
|
249
|
+
const activeBtnTopic = _btnSubTopic || S.subscribeTopic;
|
|
250
|
+
S._activeBtnTopic = activeBtnTopic;
|
|
251
|
+
|
|
252
|
+
broker.subscribe(activeBtnTopic, cfg.qos, function (topic, payload) {
|
|
253
|
+
const result = btnProfile.parsePayload(
|
|
254
|
+
String(payload), fixtureId, S.buttonPayload
|
|
255
|
+
);
|
|
256
|
+
if (result && result.match && result.state === 'on') handlePress('physical');
|
|
207
257
|
}, node.id);
|
|
208
258
|
|
|
209
259
|
// Subscribe to HA UI button cmd topic
|
|
@@ -218,9 +268,10 @@ module.exports = function (RED) {
|
|
|
218
268
|
|
|
219
269
|
// ── Device remove ─────────────────────────────────────────
|
|
220
270
|
function handleDeviceRemove() {
|
|
271
|
+
unregisterFixtureId();
|
|
221
272
|
pub(cfgTopic, '', true);
|
|
222
273
|
pub(uiBtnCfgTopic, '', true);
|
|
223
|
-
broker.unsubscribe(S.subscribeTopic, node.id);
|
|
274
|
+
broker.unsubscribe(S._activeBtnTopic || S.subscribeTopic, node.id);
|
|
224
275
|
broker.unsubscribe(uiBtnCmdTopic, node.id);
|
|
225
276
|
setStatus('red', 'ring', `${fixtureId} removed`);
|
|
226
277
|
node.log(`${fixtureId} device removed`);
|
|
@@ -246,7 +297,7 @@ module.exports = function (RED) {
|
|
|
246
297
|
|
|
247
298
|
// ── Cleanup ───────────────────────────────────────────────
|
|
248
299
|
node.on('close', function (done) {
|
|
249
|
-
broker.unsubscribe(S.subscribeTopic, node.id);
|
|
300
|
+
broker.unsubscribe(S._activeBtnTopic || S.subscribeTopic, node.id);
|
|
250
301
|
broker.unsubscribe(uiBtnCmdTopic, node.id);
|
|
251
302
|
broker.deregister(node, done);
|
|
252
303
|
});
|
|
@@ -212,12 +212,12 @@
|
|
|
212
212
|
</div>
|
|
213
213
|
<div class="form-row">
|
|
214
214
|
<label for="node-config-input-dmxFloor">
|
|
215
|
-
<i class="fa fa-arrow-
|
|
215
|
+
<i class="fa fa-arrow-up"></i> DMX Floor
|
|
216
216
|
</label>
|
|
217
217
|
<input type="number" id="node-config-input-dmxFloor"
|
|
218
|
-
min="0" max="
|
|
218
|
+
min="0" max="50" step="1" style="width:70px" />
|
|
219
219
|
<span style="margin-left:8px; color:#999; font-size:0.85em;">
|
|
220
|
-
|
|
220
|
+
Minimum DMX value sent when light is ON. Values below floor snap up to floor. 0 = disabled. Default: 3
|
|
221
221
|
</span>
|
|
222
222
|
</div>
|
|
223
223
|
</script>
|
|
@@ -317,7 +317,7 @@
|
|
|
317
317
|
</div>
|
|
318
318
|
|
|
319
319
|
<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.
|
|
320
|
+
node-red-contrib-dmx-for-ha v0.6.1
|
|
321
321
|
</div>
|
|
322
322
|
|
|
323
323
|
</script>
|
|
@@ -136,6 +136,31 @@ module.exports = function (RED) {
|
|
|
136
136
|
|
|
137
137
|
// ── Group identity and topics ─────────────────────────────────────────
|
|
138
138
|
const groupId = `LG-${S.uid}${S.uidPostfix}`;
|
|
139
|
+
// ── Fixture ID duplicate detection ──────────────────────────────────
|
|
140
|
+
function registerFixtureId() {
|
|
141
|
+
const globalCtx = node.context().global;
|
|
142
|
+
const regKey = `dmx_fixture_ids_${cfg.siteId}`;
|
|
143
|
+
let reg = {};
|
|
144
|
+
try { reg = globalCtx.get(regKey) || {}; } catch(e) {}
|
|
145
|
+
if (reg[groupId] && reg[groupId] !== node.id) {
|
|
146
|
+
node.warn(`${groupId} ⚠ DUPLICATE FIXTURE ID — already registered by node ${reg[groupId]}`);
|
|
147
|
+
setStatus('red', 'dot', `DUPLICATE ID: ${groupId}`);
|
|
148
|
+
} else {
|
|
149
|
+
reg[groupId] = node.id;
|
|
150
|
+
try { globalCtx.set(regKey, reg); } catch(e) {}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
function unregisterFixtureId() {
|
|
154
|
+
const globalCtx = node.context().global;
|
|
155
|
+
const regKey = `dmx_fixture_ids_${cfg.siteId}`;
|
|
156
|
+
let reg = {};
|
|
157
|
+
try { reg = globalCtx.get(regKey) || {}; } catch(e) {}
|
|
158
|
+
if (reg[groupId] === node.id) {
|
|
159
|
+
delete reg[groupId];
|
|
160
|
+
try { globalCtx.set(regKey, reg); } catch(e) {}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
139
164
|
const objectId = `lg_${S.uid}${S.uidPostfix}`.toLowerCase().replace(/[^a-z0-9_]/g, '_');
|
|
140
165
|
const groupTopic = cfg.buildTopic(cfg.discoveryPrefix, 'light', groupId);
|
|
141
166
|
const cfgTopic = `${groupTopic}/${cfg.configTopic}`;
|
|
@@ -252,6 +277,7 @@ module.exports = function (RED) {
|
|
|
252
277
|
|
|
253
278
|
// ── Device add ────────────────────────────────────────────
|
|
254
279
|
function handleDeviceAdd(incomingTrace) {
|
|
280
|
+
registerFixtureId();
|
|
255
281
|
if (!ctxGet('state') && !ctxGet('state', 'disk_values')) {
|
|
256
282
|
ctxSet('state', S.defaultState);
|
|
257
283
|
}
|
package/nodes/ha-mqtt-dmx.html
CHANGED
|
@@ -158,8 +158,9 @@
|
|
|
158
158
|
chWhite: { value: '' },
|
|
159
159
|
chWarmWhite: { value: '' },
|
|
160
160
|
// DMX controller
|
|
161
|
-
controllerNum:
|
|
162
|
-
universe:
|
|
161
|
+
controllerNum: { value: '1' },
|
|
162
|
+
universe: { value: '1' },
|
|
163
|
+
controllerProfile: { value: 'etherten_v1' },
|
|
163
164
|
// Options
|
|
164
165
|
haIcon: { value: 'mdi:lightbulb' },
|
|
165
166
|
showEffects: { value: true },
|
|
@@ -168,7 +169,6 @@
|
|
|
168
169
|
defaultState: { value: 'OFF' },
|
|
169
170
|
// Advanced
|
|
170
171
|
dmxLimiter: { value: '255' },
|
|
171
|
-
minOutput: { value: '1' },
|
|
172
172
|
brightBump: { value: '50' },
|
|
173
173
|
ticksPerSec: { value: '31' },
|
|
174
174
|
debugMode: { value: false },
|
|
@@ -378,6 +378,36 @@
|
|
|
378
378
|
</label>
|
|
379
379
|
</div>
|
|
380
380
|
|
|
381
|
+
<div class="form-row">
|
|
382
|
+
<label for="node-input-controllerProfile"><i class="fa fa-microchip"></i> Profile</label>
|
|
383
|
+
<select id="node-input-controllerProfile" style="width:65%" onchange="
|
|
384
|
+
var v = this.value;
|
|
385
|
+
document.getElementById('dmx-v2-note').style.display = (v === 'etherten_v2') ? '' : 'none';
|
|
386
|
+
document.getElementById('dmx-custom-row').style.display = (v === 'custom') ? '' : 'none';
|
|
387
|
+
">
|
|
388
|
+
<option value="etherten_v1">EtherTen v1 — Legacy "212255" (one msg per channel)</option>
|
|
389
|
+
<option value="etherten_v2">EtherTen v2 — Batched [[212,255],...] (one msg per tick)</option>
|
|
390
|
+
<option value="mqtt_json">MQTT JSON — {"channel":212,"value":255}</option>
|
|
391
|
+
<option value="custom">Custom payload</option>
|
|
392
|
+
</select>
|
|
393
|
+
</div>
|
|
394
|
+
|
|
395
|
+
<div class="form-row" id="dmx-v2-note" style="display:none">
|
|
396
|
+
<label></label>
|
|
397
|
+
<span style="color:#999; font-size:0.85em;">
|
|
398
|
+
<i class="fa fa-info-circle"></i>
|
|
399
|
+
v2 batches all channel changes into one MQTT message per tick.
|
|
400
|
+
Requires MW3D DMXController firmware v3.0+.
|
|
401
|
+
Significantly reduces MQTT traffic during effects and transitions.
|
|
402
|
+
</span>
|
|
403
|
+
</div>
|
|
404
|
+
|
|
405
|
+
<div class="form-row" id="dmx-custom-row" style="display:none">
|
|
406
|
+
<label for="node-input-customTopic"><i class="fa fa-exchange"></i> Custom topic</label>
|
|
407
|
+
<input type="text" id="node-input-customTopic"
|
|
408
|
+
placeholder="{siteId}/{zone}/dmx/{universe}" style="width:55%" />
|
|
409
|
+
</div>
|
|
410
|
+
|
|
381
411
|
<div class="form-row">
|
|
382
412
|
<label for="node-input-controllerNum">
|
|
383
413
|
<i class="fa fa-sort-numeric-asc"></i> Controller
|
|
@@ -512,16 +542,6 @@
|
|
|
512
542
|
min="0" max="255" style="width:70px" />
|
|
513
543
|
<span style="margin-left:8px; color:#999; font-size:0.85em;">Caps all DMX channel values (0–255)</span>
|
|
514
544
|
</div>
|
|
515
|
-
|
|
516
|
-
<div class="form-row">
|
|
517
|
-
<label for="node-input-minOutput">
|
|
518
|
-
<i class="fa fa-sort-numeric-asc"></i> Min output when ON
|
|
519
|
-
</label>
|
|
520
|
-
<input type="number" id="node-input-minOutput"
|
|
521
|
-
min="0" max="255" style="width:70px" />
|
|
522
|
-
<span style="margin-left:8px; color:#999; font-size:0.85em;">0 = pure gamma</span>
|
|
523
|
-
</div>
|
|
524
|
-
|
|
525
545
|
<div class="form-row">
|
|
526
546
|
<label for="node-input-brightBump">
|
|
527
547
|
<i class="fa fa-sort-numeric-asc"></i> Brightness bump
|
|
@@ -553,7 +573,7 @@
|
|
|
553
573
|
</div>
|
|
554
574
|
|
|
555
575
|
<div style="margin-top:16px; padding-top:8px; border-top:1px solid #444; color:#666; font-size:0.8em; text-align:right;">
|
|
556
|
-
node-red-contrib-dmx-for-ha v0.
|
|
576
|
+
node-red-contrib-dmx-for-ha v0.6.1
|
|
557
577
|
</div>
|
|
558
578
|
|
|
559
579
|
</script>
|
package/nodes/ha-mqtt-dmx.js
CHANGED
|
@@ -124,14 +124,15 @@ module.exports = function (RED) {
|
|
|
124
124
|
warmWhite:parseInt(config.chWarmWhite)|| 0,
|
|
125
125
|
},
|
|
126
126
|
controllerNum: config.controllerNum || '1',
|
|
127
|
-
universe:
|
|
127
|
+
universe: config.universe || '1',
|
|
128
|
+
controllerProfile: config.controllerProfile || 'etherten_v1',
|
|
128
129
|
haIcon: config.haIcon || 'mdi:lightbulb',
|
|
129
130
|
showEffects: config.showEffects !== false,
|
|
130
131
|
transitions: config.transitions !== false,
|
|
131
132
|
groupSync: config.groupSync === true,
|
|
132
133
|
defaultState: config.defaultState || 'OFF',
|
|
133
134
|
dmxLimiter: parseInt(config.dmxLimiter) || 255,
|
|
134
|
-
minOutput
|
|
135
|
+
// minOutput removed v0.6.2 — use dmxFloor instead
|
|
135
136
|
brightBump: parseInt(config.brightBump) || 50,
|
|
136
137
|
ticksPerSec: parseInt(config.ticksPerSec) || 31,
|
|
137
138
|
debugMode: config.debugMode === true,
|
|
@@ -204,13 +205,10 @@ module.exports = function (RED) {
|
|
|
204
205
|
brightness = brightness !== undefined ? brightness : 255;
|
|
205
206
|
const limited = Math.round((colorValue / 255) * (brightness / 255) * S.dmxLimiter);
|
|
206
207
|
const gamma = GAMMA_TABLE[Math.max(0, Math.min(255, limited))];
|
|
207
|
-
//
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
// DMX floor — snap up if above 0 but below hardware stable threshold
|
|
212
|
-
if (gamma > 0 && gamma < S.dmxFloor) return S.dmxFloor;
|
|
213
|
-
// Note: ceiling is handled by S.dmxLimiter in scaleToDmx above
|
|
208
|
+
// DMX floor — if source is ON but output is below floor, snap up
|
|
209
|
+
// Prevents flickering at low DMX values that decoders can't handle cleanly
|
|
210
|
+
// Exception: if colorValue is 0, light is intentionally OFF — send 0
|
|
211
|
+
if (colorValue > 0 && gamma < S.dmxFloor) return S.dmxFloor;
|
|
214
212
|
return gamma;
|
|
215
213
|
}
|
|
216
214
|
|
|
@@ -224,18 +222,41 @@ module.exports = function (RED) {
|
|
|
224
222
|
const _lastSent = {};
|
|
225
223
|
|
|
226
224
|
function sendDmxChannels(channels) {
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
if (
|
|
233
|
-
|
|
225
|
+
if (dmxProfile.batched) {
|
|
226
|
+
// ── Batched mode (etherten_v2) ─────────────────────────────
|
|
227
|
+
// Collect all changed channels → publish ONE message per tick
|
|
228
|
+
const batch = [];
|
|
229
|
+
channels.forEach(function ([ch, val]) {
|
|
230
|
+
if (!ch || ch <= 0 || val == null) return;
|
|
231
|
+
if (_lastSent[ch] === val) {
|
|
232
|
+
if (S.debugMode) node.warn(`[DEBUG] ${fixtureId} → SKIP ch${ch}=${val} (unchanged)`);
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
_lastSent[ch] = val;
|
|
236
|
+
batch.push([ch, val]);
|
|
237
|
+
});
|
|
238
|
+
if (batch.length > 0) {
|
|
239
|
+
const payload = dmxProfile.buildPayload(null, null, batch);
|
|
240
|
+
broker.publish({ topic: dmxTopic, payload, qos: cfg.qos, retain: false });
|
|
241
|
+
if (S.debugMode) node.warn(`[DEBUG] ${fixtureId} → ${dmxTopic} batch[${batch.length}] ${payload}`);
|
|
234
242
|
}
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
243
|
+
} else {
|
|
244
|
+
// ── Single message per channel (etherten_v1 legacy) ────────
|
|
245
|
+
channels.forEach(function ([ch, val]) {
|
|
246
|
+
if (!ch || ch <= 0 || val == null) return;
|
|
247
|
+
if (_lastSent[ch] === val) {
|
|
248
|
+
if (S.debugMode) node.warn(`[DEBUG] ${fixtureId} → SKIP ch${ch}=${val} (unchanged)`);
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
_lastSent[ch] = val;
|
|
252
|
+
const payload = dmxProfile.buildPayload
|
|
253
|
+
? dmxProfile.buildPayload(ch, val, null)
|
|
254
|
+
: buildDmxPayload(ch, val);
|
|
255
|
+
if (payload === null) return;
|
|
256
|
+
broker.publish({ topic: dmxTopic, payload, qos: cfg.qos, retain: false });
|
|
257
|
+
if (S.debugMode) node.warn(`[DEBUG] ${fixtureId} → ${dmxTopic} "${payload}"`);
|
|
258
|
+
});
|
|
259
|
+
}
|
|
239
260
|
}
|
|
240
261
|
|
|
241
262
|
function resetLastSent() {
|
|
@@ -373,7 +394,11 @@ module.exports = function (RED) {
|
|
|
373
394
|
const progress = Math.min(1, tick / totalTicks);
|
|
374
395
|
const channels = fromChannels.map(function ([ch, from], i) {
|
|
375
396
|
const to = toChannels[i] ? toChannels[i][1] : 0;
|
|
376
|
-
|
|
397
|
+
const raw = Math.round(from + (to - from) * progress);
|
|
398
|
+
// Apply floor during transition — never send flicker values
|
|
399
|
+
// Exception: if target is 0, allow 0 (light intentionally off)
|
|
400
|
+
const floored = (raw > 0 && raw < S.dmxFloor) ? S.dmxFloor : raw;
|
|
401
|
+
return [ch, floored];
|
|
377
402
|
});
|
|
378
403
|
sendDmxChannels(channels);
|
|
379
404
|
if (tick >= totalTicks) {
|
|
@@ -561,14 +586,61 @@ module.exports = function (RED) {
|
|
|
561
586
|
|
|
562
587
|
|
|
563
588
|
|
|
564
|
-
|
|
589
|
+
|
|
590
|
+
// ── DMX Controller profile ────────────────────────────────────────
|
|
591
|
+
let _dmxProfile;
|
|
592
|
+
try {
|
|
593
|
+
const _profiles = require('../profiles/index');
|
|
594
|
+
_dmxProfile = (_profiles.dmx || []).find(p => p.id === S.controllerProfile)
|
|
595
|
+
|| _profiles.dmx[0];
|
|
596
|
+
} catch(e) {
|
|
597
|
+
// Fallback to etherten_v1 if profiles not found
|
|
598
|
+
_dmxProfile = {
|
|
599
|
+
id: 'etherten_v1',
|
|
600
|
+
batched: false,
|
|
601
|
+
buildPayload: (ch, val) =>
|
|
602
|
+
String(ch).padStart(3,'0') + String(val).padStart(3,'0'),
|
|
603
|
+
buildTopic: (cfg, universe) =>
|
|
604
|
+
cfg.buildTopic(cfg.siteId, cfg.zone, 'dmx', universe)
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
const dmxProfile = _dmxProfile;
|
|
608
|
+
|
|
609
|
+
// ── Fixture identity and topics ───────────────────────────────────────
|
|
565
610
|
const fixtureId = `${S.uidPrefix}-${S.uid}${S.uidPostfix}`;
|
|
611
|
+
|
|
612
|
+
// ── Fixture ID duplicate detection ──────────────────────────────────
|
|
613
|
+
function registerFixtureId() {
|
|
614
|
+
const globalCtx = node.context().global;
|
|
615
|
+
const regKey = `dmx_fixture_ids_${cfg.siteId}`;
|
|
616
|
+
let reg = {};
|
|
617
|
+
try { reg = globalCtx.get(regKey) || {}; } catch(e) {}
|
|
618
|
+
if (reg[fixtureId] && reg[fixtureId] !== node.id) {
|
|
619
|
+
node.warn(`${fixtureId} ⚠ DUPLICATE FIXTURE ID — already registered by node ${reg[fixtureId]}`);
|
|
620
|
+
setStatus('red', 'dot', `DUPLICATE ID: ${fixtureId}`);
|
|
621
|
+
} else {
|
|
622
|
+
reg[fixtureId] = node.id;
|
|
623
|
+
try { globalCtx.set(regKey, reg); } catch(e) {}
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
function unregisterFixtureId() {
|
|
627
|
+
const globalCtx = node.context().global;
|
|
628
|
+
const regKey = `dmx_fixture_ids_${cfg.siteId}`;
|
|
629
|
+
let reg = {};
|
|
630
|
+
try { reg = globalCtx.get(regKey) || {}; } catch(e) {}
|
|
631
|
+
if (reg[fixtureId] === node.id) {
|
|
632
|
+
delete reg[fixtureId];
|
|
633
|
+
try { globalCtx.set(regKey, reg); } catch(e) {}
|
|
634
|
+
}
|
|
635
|
+
}
|
|
566
636
|
const objectId = `${S.uidPrefix}_${S.uid}${S.uidPostfix}`.toLowerCase().replace(/[^a-z0-9_]/g, '_');
|
|
567
637
|
const fixtureTopic = cfg.buildTopic(cfg.discoveryPrefix, 'light', fixtureId);
|
|
568
638
|
const cfgTopic = `${fixtureTopic}/${cfg.configTopic}`;
|
|
569
639
|
const statTopic = `${fixtureTopic}/${cfg.stateTopic}`;
|
|
570
640
|
const cmdTopic = `${fixtureTopic}/${cfg.commandTopic}`;
|
|
571
|
-
const dmxTopic =
|
|
641
|
+
const dmxTopic = dmxProfile.buildTopic
|
|
642
|
+
? dmxProfile.buildTopic(cfg, S.universe)
|
|
643
|
+
: cfg.buildTopic(cfg.siteId, cfg.zone, 'dmx', S.universe);
|
|
572
644
|
|
|
573
645
|
// ── DMX channel conflict detection ───────────────────────────────────
|
|
574
646
|
function checkChannelConflicts() {
|
|
@@ -613,6 +685,7 @@ module.exports = function (RED) {
|
|
|
613
685
|
|
|
614
686
|
// ── Device add ────────────────────────────────────────────
|
|
615
687
|
function handleDeviceAdd() {
|
|
688
|
+
registerFixtureId();
|
|
616
689
|
if (ctxGet('state') === undefined && ctxGet('state', 'disk_values') === undefined) {
|
|
617
690
|
ctxSet('state', S.defaultState);
|
|
618
691
|
}
|
|
@@ -703,6 +776,7 @@ module.exports = function (RED) {
|
|
|
703
776
|
|
|
704
777
|
// ── Device remove ─────────────────────────────────────────
|
|
705
778
|
function handleDeviceRemove() {
|
|
779
|
+
unregisterFixtureId();
|
|
706
780
|
clearChannelRegistry();
|
|
707
781
|
stopEffect();
|
|
708
782
|
if (diskTimer) {
|
package/nodes/ha-mqtt-pir.html
CHANGED
|
@@ -41,8 +41,9 @@
|
|
|
41
41
|
subLocation: { value: '' },
|
|
42
42
|
// PIR specifics
|
|
43
43
|
pirType: { value: 'Ceiling' },
|
|
44
|
-
pirPayload:
|
|
45
|
-
subscribeTopic:
|
|
44
|
+
pirPayload: { value: '', required: true },
|
|
45
|
+
subscribeTopic: { value: '', required: true },
|
|
46
|
+
controllerProfile: { value: 'mw3d_v1' },
|
|
46
47
|
// Options
|
|
47
48
|
haIcon: { value: 'mdi:motion-sensor' },
|
|
48
49
|
cableColor: { value: 'Purple' },
|
|
@@ -237,14 +238,37 @@
|
|
|
237
238
|
</div>
|
|
238
239
|
|
|
239
240
|
<div class="form-row">
|
|
241
|
+
<label for="node-input-controllerProfile"><i class="fa fa-microchip"></i> Profile</label>
|
|
242
|
+
<select id="node-input-controllerProfile" style="width:65%" onchange="
|
|
243
|
+
var v = this.value;
|
|
244
|
+
document.getElementById('pir-payload-row').style.display = (v === 'mw3d_v1' || v === 'custom') ? '' : 'none';
|
|
245
|
+
document.getElementById('pir-topic-row').style.display = (v === 'mw3d_v1' || v === 'custom') ? '' : 'none';
|
|
246
|
+
document.getElementById('pir-v2-note').style.display = (v === 'mw3d_v2') ? '' : 'none';
|
|
247
|
+
">
|
|
248
|
+
<option value="mw3d_v1">MW3D v1 — Legacy (panelId-GPIO, motion only)</option>
|
|
249
|
+
<option value="mw3d_v2">MW3D v2 — JSON (fixture ID + clear event)</option>
|
|
250
|
+
<option value="custom">Custom</option>
|
|
251
|
+
</select>
|
|
252
|
+
</div>
|
|
253
|
+
|
|
254
|
+
<div class="form-row" id="pir-v2-note" style="display:none">
|
|
255
|
+
<label></label>
|
|
256
|
+
<span style="color:#999; font-size:0.85em;">
|
|
257
|
+
<i class="fa fa-info-circle"></i>
|
|
258
|
+
v2 firmware — topic and payload auto-configured. Includes motion clear event
|
|
259
|
+
(hold timer optional). No manual payload or topic needed.
|
|
260
|
+
</span>
|
|
261
|
+
</div>
|
|
262
|
+
|
|
263
|
+
<div class="form-row" id="pir-payload-row">
|
|
240
264
|
<label for="node-input-pirPayload"><i class="fa fa-podcast"></i> Payload</label>
|
|
241
|
-
<input type="text" id="node-input-pirPayload" placeholder="Required — e.g.
|
|
265
|
+
<input type="text" id="node-input-pirPayload" placeholder="Required — e.g. 21-62" style="width:120px" />
|
|
242
266
|
<span style="margin-left:8px; color:#999; font-size:0.85em;">panelId-GPIOpin</span>
|
|
243
267
|
</div>
|
|
244
268
|
|
|
245
|
-
<div class="form-row">
|
|
269
|
+
<div class="form-row" id="pir-topic-row">
|
|
246
270
|
<label for="node-input-subscribeTopic"><i class="fa fa-exchange"></i> Subscribe topic</label>
|
|
247
|
-
<input type="text" id="node-input-subscribeTopic" placeholder="Required — e.g.
|
|
271
|
+
<input type="text" id="node-input-subscribeTopic" placeholder="Required — e.g. MW3D/Master/PIR_Sensors" style="width:55%" />
|
|
248
272
|
</div>
|
|
249
273
|
|
|
250
274
|
<hr/>
|
|
@@ -294,7 +318,7 @@
|
|
|
294
318
|
</div>
|
|
295
319
|
|
|
296
320
|
<div style="margin-top:16px; padding-top:8px; border-top:1px solid #444; color:#666; font-size:0.8em; text-align:right;">
|
|
297
|
-
node-red-contrib-dmx-for-ha v0.
|
|
321
|
+
node-red-contrib-dmx-for-ha v0.6.1
|
|
298
322
|
</div>
|
|
299
323
|
|
|
300
324
|
</script>
|