node-red-contrib-dmx-for-ha 0.5.2 → 0.6.1
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-dmx-group.html +1 -1
- package/nodes/ha-mqtt-dmx-group.js +26 -0
- package/nodes/ha-mqtt-dmx.html +34 -3
- package/nodes/ha-mqtt-dmx.js +87 -14
- 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
|
});
|
|
@@ -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 },
|
|
@@ -378,6 +379,36 @@
|
|
|
378
379
|
</label>
|
|
379
380
|
</div>
|
|
380
381
|
|
|
382
|
+
<div class="form-row">
|
|
383
|
+
<label for="node-input-controllerProfile"><i class="fa fa-microchip"></i> Profile</label>
|
|
384
|
+
<select id="node-input-controllerProfile" style="width:65%" onchange="
|
|
385
|
+
var v = this.value;
|
|
386
|
+
document.getElementById('dmx-v2-note').style.display = (v === 'etherten_v2') ? '' : 'none';
|
|
387
|
+
document.getElementById('dmx-custom-row').style.display = (v === 'custom') ? '' : 'none';
|
|
388
|
+
">
|
|
389
|
+
<option value="etherten_v1">EtherTen v1 — Legacy "212255" (one msg per channel)</option>
|
|
390
|
+
<option value="etherten_v2">EtherTen v2 — Batched [[212,255],...] (one msg per tick)</option>
|
|
391
|
+
<option value="mqtt_json">MQTT JSON — {"channel":212,"value":255}</option>
|
|
392
|
+
<option value="custom">Custom payload</option>
|
|
393
|
+
</select>
|
|
394
|
+
</div>
|
|
395
|
+
|
|
396
|
+
<div class="form-row" id="dmx-v2-note" style="display:none">
|
|
397
|
+
<label></label>
|
|
398
|
+
<span style="color:#999; font-size:0.85em;">
|
|
399
|
+
<i class="fa fa-info-circle"></i>
|
|
400
|
+
v2 batches all channel changes into one MQTT message per tick.
|
|
401
|
+
Requires MW3D DMXController firmware v3.0+.
|
|
402
|
+
Significantly reduces MQTT traffic during effects and transitions.
|
|
403
|
+
</span>
|
|
404
|
+
</div>
|
|
405
|
+
|
|
406
|
+
<div class="form-row" id="dmx-custom-row" style="display:none">
|
|
407
|
+
<label for="node-input-customTopic"><i class="fa fa-exchange"></i> Custom topic</label>
|
|
408
|
+
<input type="text" id="node-input-customTopic"
|
|
409
|
+
placeholder="{siteId}/{zone}/dmx/{universe}" style="width:55%" />
|
|
410
|
+
</div>
|
|
411
|
+
|
|
381
412
|
<div class="form-row">
|
|
382
413
|
<label for="node-input-controllerNum">
|
|
383
414
|
<i class="fa fa-sort-numeric-asc"></i> Controller
|
|
@@ -553,7 +584,7 @@
|
|
|
553
584
|
</div>
|
|
554
585
|
|
|
555
586
|
<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.
|
|
587
|
+
node-red-contrib-dmx-for-ha v0.6.1
|
|
557
588
|
</div>
|
|
558
589
|
|
|
559
590
|
</script>
|
package/nodes/ha-mqtt-dmx.js
CHANGED
|
@@ -124,7 +124,8 @@ 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,
|
|
@@ -224,18 +225,41 @@ module.exports = function (RED) {
|
|
|
224
225
|
const _lastSent = {};
|
|
225
226
|
|
|
226
227
|
function sendDmxChannels(channels) {
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
if (
|
|
233
|
-
|
|
228
|
+
if (dmxProfile.batched) {
|
|
229
|
+
// ── Batched mode (etherten_v2) ─────────────────────────────
|
|
230
|
+
// Collect all changed channels → publish ONE message per tick
|
|
231
|
+
const batch = [];
|
|
232
|
+
channels.forEach(function ([ch, val]) {
|
|
233
|
+
if (!ch || ch <= 0 || val == null) return;
|
|
234
|
+
if (_lastSent[ch] === val) {
|
|
235
|
+
if (S.debugMode) node.warn(`[DEBUG] ${fixtureId} → SKIP ch${ch}=${val} (unchanged)`);
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
_lastSent[ch] = val;
|
|
239
|
+
batch.push([ch, val]);
|
|
240
|
+
});
|
|
241
|
+
if (batch.length > 0) {
|
|
242
|
+
const payload = dmxProfile.buildPayload(null, null, batch);
|
|
243
|
+
broker.publish({ topic: dmxTopic, payload, qos: cfg.qos, retain: false });
|
|
244
|
+
if (S.debugMode) node.warn(`[DEBUG] ${fixtureId} → ${dmxTopic} batch[${batch.length}] ${payload}`);
|
|
234
245
|
}
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
246
|
+
} else {
|
|
247
|
+
// ── Single message per channel (etherten_v1 legacy) ────────
|
|
248
|
+
channels.forEach(function ([ch, val]) {
|
|
249
|
+
if (!ch || ch <= 0 || val == null) return;
|
|
250
|
+
if (_lastSent[ch] === val) {
|
|
251
|
+
if (S.debugMode) node.warn(`[DEBUG] ${fixtureId} → SKIP ch${ch}=${val} (unchanged)`);
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
_lastSent[ch] = val;
|
|
255
|
+
const payload = dmxProfile.buildPayload
|
|
256
|
+
? dmxProfile.buildPayload(ch, val, null)
|
|
257
|
+
: buildDmxPayload(ch, val);
|
|
258
|
+
if (payload === null) return;
|
|
259
|
+
broker.publish({ topic: dmxTopic, payload, qos: cfg.qos, retain: false });
|
|
260
|
+
if (S.debugMode) node.warn(`[DEBUG] ${fixtureId} → ${dmxTopic} "${payload}"`);
|
|
261
|
+
});
|
|
262
|
+
}
|
|
239
263
|
}
|
|
240
264
|
|
|
241
265
|
function resetLastSent() {
|
|
@@ -561,14 +585,61 @@ module.exports = function (RED) {
|
|
|
561
585
|
|
|
562
586
|
|
|
563
587
|
|
|
564
|
-
|
|
588
|
+
|
|
589
|
+
// ── DMX Controller profile ────────────────────────────────────────
|
|
590
|
+
let _dmxProfile;
|
|
591
|
+
try {
|
|
592
|
+
const _profiles = require('../profiles/index');
|
|
593
|
+
_dmxProfile = (_profiles.dmx || []).find(p => p.id === S.controllerProfile)
|
|
594
|
+
|| _profiles.dmx[0];
|
|
595
|
+
} catch(e) {
|
|
596
|
+
// Fallback to etherten_v1 if profiles not found
|
|
597
|
+
_dmxProfile = {
|
|
598
|
+
id: 'etherten_v1',
|
|
599
|
+
batched: false,
|
|
600
|
+
buildPayload: (ch, val) =>
|
|
601
|
+
String(ch).padStart(3,'0') + String(val).padStart(3,'0'),
|
|
602
|
+
buildTopic: (cfg, universe) =>
|
|
603
|
+
cfg.buildTopic(cfg.siteId, cfg.zone, 'dmx', universe)
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
const dmxProfile = _dmxProfile;
|
|
607
|
+
|
|
608
|
+
// ── Fixture identity and topics ───────────────────────────────────────
|
|
565
609
|
const fixtureId = `${S.uidPrefix}-${S.uid}${S.uidPostfix}`;
|
|
610
|
+
|
|
611
|
+
// ── Fixture ID duplicate detection ──────────────────────────────────
|
|
612
|
+
function registerFixtureId() {
|
|
613
|
+
const globalCtx = node.context().global;
|
|
614
|
+
const regKey = `dmx_fixture_ids_${cfg.siteId}`;
|
|
615
|
+
let reg = {};
|
|
616
|
+
try { reg = globalCtx.get(regKey) || {}; } catch(e) {}
|
|
617
|
+
if (reg[fixtureId] && reg[fixtureId] !== node.id) {
|
|
618
|
+
node.warn(`${fixtureId} ⚠ DUPLICATE FIXTURE ID — already registered by node ${reg[fixtureId]}`);
|
|
619
|
+
setStatus('red', 'dot', `DUPLICATE ID: ${fixtureId}`);
|
|
620
|
+
} else {
|
|
621
|
+
reg[fixtureId] = node.id;
|
|
622
|
+
try { globalCtx.set(regKey, reg); } catch(e) {}
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
function unregisterFixtureId() {
|
|
626
|
+
const globalCtx = node.context().global;
|
|
627
|
+
const regKey = `dmx_fixture_ids_${cfg.siteId}`;
|
|
628
|
+
let reg = {};
|
|
629
|
+
try { reg = globalCtx.get(regKey) || {}; } catch(e) {}
|
|
630
|
+
if (reg[fixtureId] === node.id) {
|
|
631
|
+
delete reg[fixtureId];
|
|
632
|
+
try { globalCtx.set(regKey, reg); } catch(e) {}
|
|
633
|
+
}
|
|
634
|
+
}
|
|
566
635
|
const objectId = `${S.uidPrefix}_${S.uid}${S.uidPostfix}`.toLowerCase().replace(/[^a-z0-9_]/g, '_');
|
|
567
636
|
const fixtureTopic = cfg.buildTopic(cfg.discoveryPrefix, 'light', fixtureId);
|
|
568
637
|
const cfgTopic = `${fixtureTopic}/${cfg.configTopic}`;
|
|
569
638
|
const statTopic = `${fixtureTopic}/${cfg.stateTopic}`;
|
|
570
639
|
const cmdTopic = `${fixtureTopic}/${cfg.commandTopic}`;
|
|
571
|
-
const dmxTopic =
|
|
640
|
+
const dmxTopic = dmxProfile.buildTopic
|
|
641
|
+
? dmxProfile.buildTopic(cfg, S.universe)
|
|
642
|
+
: cfg.buildTopic(cfg.siteId, cfg.zone, 'dmx', S.universe);
|
|
572
643
|
|
|
573
644
|
// ── DMX channel conflict detection ───────────────────────────────────
|
|
574
645
|
function checkChannelConflicts() {
|
|
@@ -613,6 +684,7 @@ module.exports = function (RED) {
|
|
|
613
684
|
|
|
614
685
|
// ── Device add ────────────────────────────────────────────
|
|
615
686
|
function handleDeviceAdd() {
|
|
687
|
+
registerFixtureId();
|
|
616
688
|
if (ctxGet('state') === undefined && ctxGet('state', 'disk_values') === undefined) {
|
|
617
689
|
ctxSet('state', S.defaultState);
|
|
618
690
|
}
|
|
@@ -703,6 +775,7 @@ module.exports = function (RED) {
|
|
|
703
775
|
|
|
704
776
|
// ── Device remove ─────────────────────────────────────────
|
|
705
777
|
function handleDeviceRemove() {
|
|
778
|
+
unregisterFixtureId();
|
|
706
779
|
clearChannelRegistry();
|
|
707
780
|
stopEffect();
|
|
708
781
|
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>
|
package/nodes/ha-mqtt-pir.js
CHANGED
|
@@ -100,8 +100,9 @@ module.exports = function (RED) {
|
|
|
100
100
|
uid: config.uid || '',
|
|
101
101
|
uidPostfix: config.uidPostfix || '',
|
|
102
102
|
pirType: config.pirType || 'Ceiling',
|
|
103
|
-
pirPayload:
|
|
104
|
-
subscribeTopic:
|
|
103
|
+
pirPayload: String(config.pirPayload || ''),
|
|
104
|
+
subscribeTopic: config.subscribeTopic || '',
|
|
105
|
+
controllerProfile: config.controllerProfile || 'mw3d_v1',
|
|
105
106
|
area: config.area || '',
|
|
106
107
|
situation: config.situation || 'in',
|
|
107
108
|
subLocation: config.subLocation || '',
|
|
@@ -127,8 +128,50 @@ module.exports = function (RED) {
|
|
|
127
128
|
}
|
|
128
129
|
|
|
129
130
|
|
|
130
|
-
|
|
131
|
+
|
|
132
|
+
// ── Controller profile ────────────────────────────────────────────
|
|
133
|
+
let _pirProfile;
|
|
134
|
+
try {
|
|
135
|
+
const _profiles = require('../profiles/index');
|
|
136
|
+
_pirProfile = (_profiles.pir || []).find(p => p.id === S.controllerProfile)
|
|
137
|
+
|| _profiles.pir[0];
|
|
138
|
+
} catch(e) {
|
|
139
|
+
_pirProfile = {
|
|
140
|
+
id: 'mw3d_v1',
|
|
141
|
+
buildTopic: () => null,
|
|
142
|
+
parsePayload: (raw, fid, exp) => raw === exp ? { match: true, state: 'on' } : null,
|
|
143
|
+
hasOffEvent: false
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
const pirProfile = _pirProfile;
|
|
147
|
+
|
|
148
|
+
// ── Fixture identity and topics ───────────────────────────────────────
|
|
131
149
|
const fixtureId = `S-${S.uid}${S.uidPostfix}`;
|
|
150
|
+
|
|
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
|
+
}
|
|
132
175
|
const objectId = `s_${S.uid}${S.uidPostfix}`.toLowerCase().replace(/[^a-z0-9_]/g, '_');
|
|
133
176
|
const fixtureTopic = cfg.buildTopic(cfg.discoveryPrefix, 'binary_sensor', fixtureId);
|
|
134
177
|
const cfgTopic = `${fixtureTopic}/${cfg.configTopic}`;
|
|
@@ -183,6 +226,14 @@ module.exports = function (RED) {
|
|
|
183
226
|
node.log(`${fixtureId} motion detected`);
|
|
184
227
|
}
|
|
185
228
|
|
|
229
|
+
function handleClear() {
|
|
230
|
+
// Explicit motion clear from v2 firmware
|
|
231
|
+
lastTrigger = 0;
|
|
232
|
+
pub(statTopic, 'OFF', false);
|
|
233
|
+
setStatus('green', 'ring', `${fixtureId} motion cleared`);
|
|
234
|
+
node.log(`${fixtureId} motion cleared`);
|
|
235
|
+
}
|
|
236
|
+
|
|
186
237
|
// ── Availability ──────────────────────────────────────────
|
|
187
238
|
function handleAvailability(payload) {
|
|
188
239
|
const status = String(payload || '').toLowerCase();
|
|
@@ -199,6 +250,7 @@ module.exports = function (RED) {
|
|
|
199
250
|
|
|
200
251
|
// ── Device add ────────────────────────────────────────────
|
|
201
252
|
function handleDeviceAdd() {
|
|
253
|
+
registerFixtureId();
|
|
202
254
|
const discovery = {
|
|
203
255
|
unique_id: fixtureId,
|
|
204
256
|
object_id: objectId,
|
|
@@ -226,10 +278,20 @@ module.exports = function (RED) {
|
|
|
226
278
|
|
|
227
279
|
pub(cfgTopic, discovery, cfg.retainDiscovery);
|
|
228
280
|
|
|
229
|
-
// Subscribe to controller topic
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
281
|
+
// Subscribe to controller topic — topic and matching via profile
|
|
282
|
+
const _pirSubTopic = pirProfile.buildTopic ? pirProfile.buildTopic(cfg) : null;
|
|
283
|
+
const activePirTopic = _pirSubTopic || S.subscribeTopic;
|
|
284
|
+
S._activePirTopic = activePirTopic;
|
|
285
|
+
|
|
286
|
+
broker.subscribe(activePirTopic, cfg.qos, function (topic, payload) {
|
|
287
|
+
const result = pirProfile.parsePayload(
|
|
288
|
+
payload.toString(), fixtureId, S.pirPayload
|
|
289
|
+
);
|
|
290
|
+
if (result && result.match) {
|
|
291
|
+
if (result.state === 'on') handleTrigger();
|
|
292
|
+
// v2 firmware sends explicit off — handle motion clear
|
|
293
|
+
else if (result.state === 'off' && pirProfile.hasOffEvent) handleClear();
|
|
294
|
+
}
|
|
233
295
|
}, node.id);
|
|
234
296
|
|
|
235
297
|
setStatus('green', 'ring', `${fixtureId} discovery sent`);
|
|
@@ -239,10 +301,11 @@ module.exports = function (RED) {
|
|
|
239
301
|
|
|
240
302
|
// ── Device remove ─────────────────────────────────────────
|
|
241
303
|
function handleDeviceRemove() {
|
|
304
|
+
unregisterFixtureId();
|
|
242
305
|
cancelWarmup();
|
|
243
306
|
pub(avtyTopic, 'offline', true);
|
|
244
307
|
pub(cfgTopic, '', true);
|
|
245
|
-
broker.unsubscribe(S.subscribeTopic, node.id);
|
|
308
|
+
broker.unsubscribe(S._activePirTopic || S.subscribeTopic, node.id);
|
|
246
309
|
setStatus('red', 'ring', `${fixtureId} removed`);
|
|
247
310
|
node.log(`${fixtureId} device removed`);
|
|
248
311
|
}
|
|
@@ -269,7 +332,7 @@ module.exports = function (RED) {
|
|
|
269
332
|
// ── Cleanup ───────────────────────────────────────────────
|
|
270
333
|
node.on('close', function (done) {
|
|
271
334
|
cancelWarmup();
|
|
272
|
-
broker.unsubscribe(S.subscribeTopic, node.id);
|
|
335
|
+
broker.unsubscribe(S._activePirTopic || S.subscribeTopic, node.id);
|
|
273
336
|
broker.deregister(node, done);
|
|
274
337
|
});
|
|
275
338
|
}
|