node-red-contrib-velbus-2026 0.8.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.
Files changed (56) hide show
  1. package/CHANGELOG_FORUM.md +784 -0
  2. package/LICENSE +21 -0
  3. package/README.md +140 -0
  4. package/examples/velbus-basic-relay-dimmer.json +328 -0
  5. package/index.js +3 -0
  6. package/lib/blind-types-20.js +26 -0
  7. package/lib/blind-types-s.js +46 -0
  8. package/lib/blind-types.js +38 -0
  9. package/lib/dimmer-types-20.js +120 -0
  10. package/lib/dimmer-types.js +34 -0
  11. package/lib/glass-panel-types.js +296 -0
  12. package/lib/pir-types-20.js +51 -0
  13. package/lib/pir-types.js +61 -0
  14. package/lib/relay-types-20.js +43 -0
  15. package/lib/relay-types.js +39 -0
  16. package/lib/sensor-types-20.js +29 -0
  17. package/lib/sensor-types.js +49 -0
  18. package/lib/velbus-utils.js +80 -0
  19. package/nodes/velbus-blind/velbus-blind.html +165 -0
  20. package/nodes/velbus-blind/velbus-blind.js +251 -0
  21. package/nodes/velbus-blind-20/velbus-blind-20.html +179 -0
  22. package/nodes/velbus-blind-20/velbus-blind-20.js +300 -0
  23. package/nodes/velbus-blind-s/velbus-blind-s.html +181 -0
  24. package/nodes/velbus-blind-s/velbus-blind-s.js +282 -0
  25. package/nodes/velbus-bridge/velbus-bridge.html +72 -0
  26. package/nodes/velbus-bridge/velbus-bridge.js +304 -0
  27. package/nodes/velbus-button/velbus-button.html +172 -0
  28. package/nodes/velbus-button/velbus-button.js +117 -0
  29. package/nodes/velbus-clock/velbus-clock.html +198 -0
  30. package/nodes/velbus-clock/velbus-clock.js +273 -0
  31. package/nodes/velbus-dimmer/velbus-dimmer.html +174 -0
  32. package/nodes/velbus-dimmer/velbus-dimmer.js +457 -0
  33. package/nodes/velbus-dimmer-20/velbus-dimmer-20.html +271 -0
  34. package/nodes/velbus-dimmer-20/velbus-dimmer-20.js +602 -0
  35. package/nodes/velbus-glass-panel/velbus-glass-panel.html +337 -0
  36. package/nodes/velbus-glass-panel/velbus-glass-panel.js +492 -0
  37. package/nodes/velbus-meteo/velbus-meteo.html +120 -0
  38. package/nodes/velbus-meteo/velbus-meteo.js +279 -0
  39. package/nodes/velbus-pir/velbus-pir.html +187 -0
  40. package/nodes/velbus-pir/velbus-pir.js +269 -0
  41. package/nodes/velbus-pir-20/velbus-pir-20.html +186 -0
  42. package/nodes/velbus-pir-20/velbus-pir-20.js +352 -0
  43. package/nodes/velbus-relay/velbus-relay.html +215 -0
  44. package/nodes/velbus-relay/velbus-relay.js +404 -0
  45. package/nodes/velbus-relay-20/velbus-relay-20.html +123 -0
  46. package/nodes/velbus-relay-20/velbus-relay-20.js +434 -0
  47. package/nodes/velbus-scan/velbus-scan.html +87 -0
  48. package/nodes/velbus-scan/velbus-scan.js +313 -0
  49. package/nodes/velbus-sensor/velbus-sensor.html +154 -0
  50. package/nodes/velbus-sensor/velbus-sensor.js +259 -0
  51. package/nodes/velbus-sensor-20/velbus-sensor-20.html +158 -0
  52. package/nodes/velbus-sensor-20/velbus-sensor-20.js +314 -0
  53. package/nodes/velbus-thermostat/velbus-thermostat.html +191 -0
  54. package/nodes/velbus-thermostat/velbus-thermostat.js +322 -0
  55. package/package.json +55 -0
  56. package/velbus-2026-repo.bundle +0 -0
@@ -0,0 +1,434 @@
1
+ 'use strict';
2
+
3
+ const { pkt, rtrPkt, parsePkt } = require('../../lib/velbus-utils');
4
+ const { RELAY_TYPES_20, RELAY_TYPE_IDS_20 } = require('../../lib/relay-types-20');
5
+
6
+ module.exports = function(RED) {
7
+
8
+ function VelbusRelay20Node(config) {
9
+ RED.nodes.createNode(this, config);
10
+ const node = this;
11
+
12
+ // Config
13
+ node.bridge = RED.nodes.getNode(config.bridge);
14
+ node.moduleAddr = parseInt(config.moduleAddr, 16);
15
+ node.startChannel = parseInt(config.startChannel) || 1;
16
+ node.channelCount = parseInt(config.channelCount) || 4;
17
+ node.nameOverride = config.name || '';
18
+
19
+ if (!node.bridge) {
20
+ node.error('velbus-relay-20: no bridge configured');
21
+ return;
22
+ }
23
+ if (isNaN(node.moduleAddr) || node.moduleAddr < 1 || node.moduleAddr > 0xFE) {
24
+ node.error('velbus-relay-20: invalid module address');
25
+ return;
26
+ }
27
+
28
+ // State — V2 modules send one 0xFB packet for the whole module
29
+ // channelState keyed by channel number (1-8)
30
+ let _channelState = {}; // { [ch]: { on, inhibited, forcedOn, forcedOff, programDisabled, timerRunning } }
31
+ let _moduleInfo = null;
32
+ let _velbusName = '';
33
+ let _nameParts = {};
34
+ let _blocked = false;
35
+ let _nameTimer = null;
36
+ let _alarmProgram = null; // byte 8 of 0xFB
37
+
38
+ // ── Helpers ───────────────────────────────────────────────────────────
39
+
40
+ function addrHex(a) {
41
+ return '0x' + a.toString(16).padStart(2, '0').toUpperCase();
42
+ }
43
+
44
+ function displayName() {
45
+ if (node.nameOverride) return node.nameOverride;
46
+ if (_velbusName) return _velbusName;
47
+ if (_moduleInfo) return _moduleInfo.name;
48
+ return addrHex(node.moduleAddr);
49
+ }
50
+
51
+ function statusPrefix() {
52
+ return displayName() + ' (' + addrHex(node.moduleAddr) + ')';
53
+ }
54
+
55
+ function setStatus(text, fill, shape) {
56
+ node.status({ fill: fill || 'grey', shape: shape || 'dot', text });
57
+ }
58
+
59
+ function channelState(chNum) {
60
+ const s = _channelState[chNum];
61
+ if (!s) return 'unknown';
62
+ if (s.forcedOff) return 'forced_off';
63
+ if (s.forcedOn) return 'forced_on';
64
+ if (s.inhibited) return 'inhibited';
65
+ if (s.timerRunning) return 'timer_running';
66
+ return s.on ? 'on' : 'off';
67
+ }
68
+
69
+ function decodeAlarmProgram(b) {
70
+ return {
71
+ program: b & 0x03, // 0=none, 1=summer, 2=winter, 3=holiday
72
+ alarm1: !!(b & 0x04),
73
+ alarm1Global: !!(b & 0x08),
74
+ alarm2: !!(b & 0x10),
75
+ alarm2Global: !!(b & 0x20),
76
+ sunrise: !!(b & 0x40),
77
+ sunset: !!(b & 0x80)
78
+ };
79
+ }
80
+
81
+ // ── Name retrieval ────────────────────────────────────────────────────
82
+
83
+ function requestName() {
84
+ // Priority 0xF8 (command) not 0xFB (status) — critical
85
+ node.bridge.send(pkt(0xF8, node.moduleAddr, [0xEF, 0xFF]));
86
+ _nameTimer = setTimeout(assembleName, 2000);
87
+ }
88
+
89
+ function assembleName() {
90
+ if (_nameTimer) { clearTimeout(_nameTimer); _nameTimer = null; }
91
+ const raw = [
92
+ ...(_nameParts[1] || []),
93
+ ...(_nameParts[2] || []),
94
+ ...(_nameParts[3] || [])
95
+ ];
96
+ const name = raw
97
+ .filter(c => c !== 0xFF && c !== 0x00)
98
+ .map(c => String.fromCharCode(c))
99
+ .join('')
100
+ .trim();
101
+ if (name) {
102
+ _velbusName = name;
103
+ node.log('velbus-relay-20: module name: ' + name);
104
+ }
105
+ requestStatus();
106
+ }
107
+
108
+ function requestStatus() {
109
+ // V2: 0xFA with don't-care byte requests full module status (all channels in one 0xFB)
110
+ node.bridge.send(pkt(0xFB, node.moduleAddr, [0xFA, 0x00]));
111
+ setStatus(statusPrefix() + ' online', 'grey');
112
+ }
113
+
114
+ // ── 0xFF Firmware check ───────────────────────────────────────────────
115
+
116
+ function handleModuleType(body) {
117
+ const typeId = body[1];
118
+
119
+ if (!RELAY_TYPE_IDS_20.has(typeId)) {
120
+ const msg = '⚠ ' + statusPrefix() + ' unknown type ' + addrHex(typeId);
121
+ setStatus(msg, 'red');
122
+ node.error('velbus-relay-20: ' + msg);
123
+ node.send([null, { payload: {
124
+ topic: 'module_unknown',
125
+ address: addrHex(node.moduleAddr),
126
+ typeId: addrHex(typeId),
127
+ message: 'Unrecognised module type — is this a -20 series relay?'
128
+ }}]);
129
+ _blocked = true;
130
+ return;
131
+ }
132
+
133
+ const typeInfo = RELAY_TYPES_20[typeId];
134
+ const serial = body.length >= 4 ? (body[2] << 8 | body[3]) : null;
135
+ const mapVer = body.length >= 5 ? body[4] : null;
136
+ const buildHi = body.length >= 6 ? body[5] : null;
137
+ const buildLo = body.length >= 7 ? body[6] : null;
138
+ const build = (buildHi !== null && buildLo !== null)
139
+ ? (buildHi * 100 + buildLo) : null;
140
+ // Byte 8 = properties (CAN FD, terminator, HW ver)
141
+ const props = body.length >= 8 ? body[7] : null;
142
+ const canFD = props !== null ? !!(props & 0x20) : false;
143
+
144
+ _moduleInfo = { ...typeInfo, typeId, serial, build, memoryMapVersion: mapVer, canFD };
145
+
146
+ const minVer = typeInfo.minMemoryMapVersion;
147
+ if (minVer !== null && mapVer !== null && mapVer < minVer) {
148
+ const msg = '⚠ ' + statusPrefix() + ' firmware too old (map v' + mapVer + ', need v' + minVer + ')';
149
+ setStatus(msg, 'red');
150
+ node.error('velbus-relay-20: ' + msg);
151
+ node.send([null, { payload: {
152
+ topic: 'firmware_incompatible',
153
+ address: addrHex(node.moduleAddr),
154
+ module: typeInfo.name,
155
+ memoryMapVersion: mapVer,
156
+ minimumRequired: minVer,
157
+ message: 'Firmware too old — update module firmware before use'
158
+ }}]);
159
+ _blocked = true;
160
+ return;
161
+ }
162
+
163
+ if (minVer === null) {
164
+ setStatus(statusPrefix() + ' map v' + mapVer + ' (unverified)', 'yellow');
165
+ node.warn('velbus-relay-20: ' + addrHex(node.moduleAddr) + ' ' + typeInfo.name +
166
+ ' map v' + mapVer + ' — minimum version unverified, proceeding');
167
+ }
168
+
169
+ _blocked = false;
170
+
171
+ node.send([{ payload: {
172
+ topic: 'module_online',
173
+ address: addrHex(node.moduleAddr),
174
+ module: typeInfo.name,
175
+ typeId: addrHex(typeId),
176
+ serial,
177
+ build,
178
+ memoryMapVersion: mapVer,
179
+ canFD
180
+ }}, null]);
181
+
182
+ node.log('velbus-relay-20: ' + typeInfo.name + ' at ' + addrHex(node.moduleAddr) +
183
+ ' serial ' + (serial ? serial.toString(16).toUpperCase() : '?') +
184
+ ' build ' + build + ' map v' + mapVer);
185
+
186
+ requestName();
187
+ }
188
+
189
+ // ── Packet handler ────────────────────────────────────────────────────
190
+
191
+ function onPacket(raw) {
192
+ const p = parsePkt(raw);
193
+ if (!p) return;
194
+ const { pri, body, cmd, rtr } = p;
195
+
196
+ // ── 0xFF Module type ─────────────────────────────────────────────────
197
+ if (cmd === 0xFF && !rtr && body.length >= 2) {
198
+ handleModuleType(body);
199
+ return;
200
+ }
201
+
202
+ if (_blocked) return;
203
+
204
+ // ── 0xF0/0xF1/0xF2 Channel name parts ───────────────────────────────
205
+ if ((cmd === 0xF0 || cmd === 0xF1 || cmd === 0xF2) && body.length >= 2) {
206
+ const part = cmd - 0xEF;
207
+ _nameParts[part] = body.slice(2);
208
+ if (part === 3) assembleName();
209
+ return;
210
+ }
211
+
212
+ // ── 0xFB Module status (V2 bitmask format) ───────────────────────────
213
+ // One packet covers all channels: separate bitmask bytes per condition
214
+ if (cmd === 0xFB && body.length >= 8) {
215
+ const onBits = body[1];
216
+ const inhibBits = body[2];
217
+ const forcedOnBits = body[3];
218
+ const forcedOffBits= body[4];
219
+ const progDisBits = body[5];
220
+ const timerBits = body[6];
221
+ const alarmProg = body[7];
222
+
223
+ _alarmProgram = decodeAlarmProgram(alarmProg);
224
+
225
+ const changedChannels = [];
226
+
227
+ for (let i = 0; i < 8; i++) {
228
+ const ch = i + 1;
229
+ if (ch < node.startChannel || ch >= node.startChannel + node.channelCount) continue;
230
+
231
+ const b = 1 << i;
232
+ const newState = {
233
+ on: !!(onBits & b),
234
+ inhibited: !!(inhibBits & b),
235
+ forcedOn: !!(forcedOnBits & b),
236
+ forcedOff: !!(forcedOffBits & b),
237
+ programDisabled:!!(progDisBits & b),
238
+ timerRunning: !!(timerBits & b)
239
+ };
240
+ _channelState[ch] = newState;
241
+ changedChannels.push(ch);
242
+ }
243
+
244
+ // Emit one status message per channel in range
245
+ for (const ch of changedChannels) {
246
+ const s = _channelState[ch];
247
+ const state = channelState(ch);
248
+ const isActive = ['on','timer_running','forced_on'].includes(state);
249
+ const isWarning = ['forced_off','inhibited'].includes(state);
250
+
251
+ setStatus(statusPrefix() + ' ch' + ch + ' ' + state,
252
+ isWarning ? 'yellow' : (isActive ? 'green' : 'grey'));
253
+
254
+ const payload = {
255
+ topic: 'relay_status',
256
+ address: addrHex(node.moduleAddr),
257
+ module: displayName(),
258
+ channel: ch,
259
+ state,
260
+ on: s.on,
261
+ inhibited: s.inhibited,
262
+ forcedOn: s.forcedOn,
263
+ forcedOff: s.forcedOff,
264
+ programDisabled:s.programDisabled,
265
+ timerRunning: s.timerRunning,
266
+ alarmProgram: _alarmProgram,
267
+ timestamp: Date.now()
268
+ };
269
+
270
+ let warnMsg = null;
271
+ if (isWarning) {
272
+ warnMsg = {
273
+ topic: 'relay_state_warning',
274
+ address: addrHex(node.moduleAddr),
275
+ module: displayName(),
276
+ channel: ch,
277
+ state,
278
+ message: 'Channel ' + ch + ' is in ' + state + ' — direct commands will be ignored by module'
279
+ };
280
+ }
281
+
282
+ node.send([{ payload }, warnMsg ? { payload: warnMsg } : null]);
283
+ }
284
+ return;
285
+ }
286
+
287
+ // ── 0x00 Channel switched broadcast ──────────────────────────────────
288
+ // Fires on local push-button-driven switches. Confirmed against
289
+ // protocol_vmb4ryld_20_vmb4ryno_20_vmb1rys_20.pdf: SID10-SID9=00 =
290
+ // highest priority = 0xF8 (see packetprotocol README priority table).
291
+ // The previous `pri === 0xFB` check required the opposite (lowest
292
+ // priority) and silently dropped every one of these broadcasts —
293
+ // nothing else in this file gates on priority, and this cmd byte has
294
+ // only one defined meaning for a relay module, so no filter is needed.
295
+ if (cmd === 0x00 && body.length >= 4) {
296
+ const onBits = body[1];
297
+ const offBits = body[2];
298
+
299
+ for (let i = 0; i < 8; i++) {
300
+ const b = 1 << i;
301
+ const ch = i + 1;
302
+ if (ch < node.startChannel || ch >= node.startChannel + node.channelCount) continue;
303
+
304
+ if (onBits & b) {
305
+ node.send([{ payload: {
306
+ topic: 'relay_switched', address: addrHex(node.moduleAddr),
307
+ module: displayName(), channel: ch, state: 'on'
308
+ }}, null]);
309
+ }
310
+ if (offBits & b) {
311
+ node.send([{ payload: {
312
+ topic: 'relay_switched', address: addrHex(node.moduleAddr),
313
+ module: displayName(), channel: ch, state: 'off'
314
+ }}, null]);
315
+ }
316
+ }
317
+ }
318
+ }
319
+
320
+ // ── Input: command encoder ────────────────────────────────────────────
321
+ // V2: channel is a number (1-8) or 0xFF for all — NOT a bitmask
322
+
323
+ node.on('input', function(msg) {
324
+ if (_blocked) {
325
+ node.warn('velbus-relay-20: commands blocked — firmware incompatible or unknown module');
326
+ return;
327
+ }
328
+
329
+ const inp = msg.payload;
330
+ if (!inp || typeof inp !== 'object') return;
331
+
332
+ const addr = inp.address !== undefined
333
+ ? (typeof inp.address === 'string' ? parseInt(inp.address, 16) : inp.address)
334
+ : node.moduleAddr;
335
+
336
+ // Channel: 1-8, or 0 / 'all' / 255 for broadcast to all channels
337
+ let ch;
338
+ if (inp.channel === 'all' || inp.channel === 0 || inp.channel === 255) {
339
+ ch = 0xFF;
340
+ } else if (typeof inp.channel === 'number' && inp.channel >= 1 && inp.channel <= 8) {
341
+ ch = inp.channel;
342
+ } else {
343
+ node.warn('velbus-relay-20: missing or invalid channel in command');
344
+ return;
345
+ }
346
+
347
+ const cmd = inp.cmd || '';
348
+ const dur = inp.duration || 0;
349
+
350
+ // Warn if specific channel is in forced/inhibited state
351
+ if (ch !== 0xFF) {
352
+ const cs = _channelState[ch];
353
+ if (cs) {
354
+ const state = channelState(ch);
355
+ if (['forced_on', 'forced_off', 'inhibited'].includes(state)) {
356
+ const cancelCmds = ['cancel_forced_on', 'cancel_forced_off', 'cancel_inhibit',
357
+ 'forced_on', 'forced_off', 'inhibit'];
358
+ if (!cancelCmds.includes(cmd)) {
359
+ node.warn('velbus-relay-20 ' + addrHex(addr) + ' ch' + ch +
360
+ ': channel is in ' + state + ' — command may be ignored by module');
361
+ }
362
+ }
363
+ }
364
+ }
365
+
366
+ function timer24(sec) {
367
+ if (!sec || sec === 0) return [0, 0, 0];
368
+ if (sec < 0) return [0xFF, 0xFF, 0xFF];
369
+ return [(sec >> 16) & 0xFF, (sec >> 8) & 0xFF, sec & 0xFF];
370
+ }
371
+
372
+ const t = timer24(dur);
373
+ let packet = null;
374
+
375
+ switch (cmd) {
376
+ case 'on': packet = pkt(0xF8, addr, [0x02, ch]); break;
377
+ case 'off': packet = pkt(0xF8, addr, [0x01, ch]); break;
378
+ case 'toggle': {
379
+ const cs = ch !== 0xFF ? _channelState[ch] : null;
380
+ const isOn = cs && cs.on;
381
+ packet = pkt(0xF8, addr, [isOn ? 0x01 : 0x02, ch]);
382
+ break;
383
+ }
384
+ case 'timer': packet = pkt(0xF8, addr, [0x03, ch, ...t]); break;
385
+ case 'interval_timer':
386
+ // REMOVED v0.7.9 — there is no live bus command for a blink/interval
387
+ // timer on the V2 relay series. Confirmed against the official
388
+ // protocol PDF (VMB4RYLD-20/VMB4RYNO-20/VMB1RYS-20, ed.3): the
389
+ // received-command list has no equivalent to the original series'
390
+ // 0x0D "Start relay blinking timer". An "interval timer running"
391
+ // status bit exists (0xFB DATABYTE7), but it can only be triggered
392
+ // by writing a Program Step (0xC0/0xC2, Action code 22 — Start/Stop
393
+ // interval timer — Time-out/Pulse time/Pause time/Relay channel)
394
+ // linked to a button or scenario. That is commissioning-agent-level
395
+ // memory work, out of scope for this node's live command set.
396
+ node.warn('velbus-relay-20: interval_timer is not a live bus command on V2 relays. ' +
397
+ 'It requires writing a Program Step (Action code 22) to module memory — ' +
398
+ 'see the node help for detail.');
399
+ return;
400
+ case 'forced_on': packet = pkt(0xF8, addr, [0x14, ch, ...t]); break;
401
+ case 'forced_off': packet = pkt(0xF8, addr, [0x12, ch, ...t]); break;
402
+ case 'cancel_forced_on': packet = pkt(0xF8, addr, [0x15, ch]); break;
403
+ case 'cancel_forced_off': packet = pkt(0xF8, addr, [0x13, ch]); break;
404
+ case 'inhibit': packet = pkt(0xF8, addr, [0x16, ch, ...t]); break;
405
+ case 'cancel_inhibit': packet = pkt(0xF8, addr, [0x17, ch]); break;
406
+ case 'status': packet = pkt(0xFB, node.moduleAddr, [0xFA, 0x00]); break;
407
+ default:
408
+ node.warn('velbus-relay-20: unknown command: ' + cmd);
409
+ return;
410
+ }
411
+
412
+ if (packet) node.bridge.send(packet);
413
+ });
414
+
415
+ // ── Startup ───────────────────────────────────────────────────────────
416
+
417
+ node.bridge.register(node.moduleAddr, onPacket);
418
+
419
+ setTimeout(() => {
420
+ node.bridge.send(rtrPkt(node.moduleAddr), true);
421
+ }, 500);
422
+
423
+ setStatus('connecting…', 'grey');
424
+
425
+ // ── Cleanup ───────────────────────────────────────────────────────────
426
+
427
+ node.on('close', function() {
428
+ if (_nameTimer) clearTimeout(_nameTimer);
429
+ node.bridge.deregister(node.moduleAddr, onPacket);
430
+ });
431
+ }
432
+
433
+ RED.nodes.registerType('velbus-relay-20', VelbusRelay20Node);
434
+ };
@@ -0,0 +1,87 @@
1
+ <script type="text/javascript">
2
+ RED.nodes.registerType('velbus-scan', {
3
+ category: 'Velbus (inputs)',
4
+ color: '#3A8C8C',
5
+ defaults: {
6
+ name: { value: '' },
7
+ bridge: { value: '', type: 'velbus-bridge', required: true },
8
+ rtrDelay: { value: 75, validate: RED.validators.number() },
9
+ collectTime: { value: 8000, validate: RED.validators.number() }
10
+ },
11
+ inputs: 1,
12
+ outputs: 2,
13
+ icon: 'font-awesome/fa-search',
14
+ label: function() { return this.name || 'velbus scan'; },
15
+ outputLabels: ['scan complete (array)', 'module found (one per module)']
16
+ });
17
+ </script>
18
+
19
+ <script type="text/html" data-template-name="velbus-scan">
20
+ <div class="form-row">
21
+ <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
22
+ <input type="text" id="node-input-name" placeholder="velbus scan">
23
+ </div>
24
+ <div class="form-row">
25
+ <label for="node-input-bridge"><i class="fa fa-server"></i> Bridge</label>
26
+ <input type="text" id="node-input-bridge">
27
+ </div>
28
+ <div class="form-row">
29
+ <label for="node-input-rtrDelay"><i class="fa fa-clock-o"></i> RTR delay (ms)</label>
30
+ <input type="number" id="node-input-rtrDelay" min="50" max="500" style="width:80px">
31
+ &nbsp;<small>Time between RTR packets (default 75ms)</small>
32
+ </div>
33
+ <div class="form-row">
34
+ <label for="node-input-collectTime"><i class="fa fa-hourglass-half"></i> Collect time (ms)</label>
35
+ <input type="number" id="node-input-collectTime" min="2000" max="30000" style="width:80px">
36
+ &nbsp;<small>Wait after last RTR for responses (default 8000ms)</small>
37
+ </div>
38
+ </script>
39
+
40
+ <script type="text/html" data-help-name="velbus-scan">
41
+ <p style="background:#fff3cd; border-left:4px solid #ffc107; padding:8px 12px; margin-bottom:12px; font-size:0.85em;">
42
+ <strong>&#9888; Testing status:</strong> This node was generated with Claude.ai and is in
43
+ need of extensive field testing before being deployed into a commercial project.
44
+ It is presented "as is" &mdash; any use beyond testing is entirely at your own risk and liability.
45
+ Constructive feedback is welcomed, accompanied by as many examples and debug captures as possible.
46
+ <a href="https://github.com/MDAR/node-red-contrib-velbus-2026/issues" target="_blank">File an issue on GitHub</a>.
47
+ </p>
48
+ <p>Scans the Velbus bus and discovers all connected modules.</p>
49
+ <p>For original series V1 modules, use <b>velbus-relay</b>. For -20 series modules, use <b>velbus-relay-20</b>.</p>
50
+
51
+ <h3>How it works</h3>
52
+ <p>Sends RTR packets to every address (0x01–0xFE) at a fixed interval.
53
+ Modules respond asynchronously — responses are collected for the
54
+ <b>Collect time</b> after the last RTR is sent.</p>
55
+ <p>Total scan time = <i>(RTR delay × 254) + collect time</i>.
56
+ At defaults: about 27 seconds.</p>
57
+
58
+ <h3>Triggering</h3>
59
+ <p>Inject any message. Payload: <code>"scan"</code>, <code>true</code>, <code>1</code>,
60
+ or <code>{ "cmd": "scan" }</code>. Send <code>"cancel"</code> to abort.</p>
61
+
62
+ <h3>Output 1 — Scan complete</h3>
63
+ <pre>
64
+ {
65
+ "topic": "scan_complete",
66
+ "count": 35,
67
+ "modules": [
68
+ {
69
+ "address": "0x03",
70
+ "addressDec": 3,
71
+ "typeId": "0x36",
72
+ "module": "VMBEL4",
73
+ "serial": "19E4",
74
+ "build": 3433,
75
+ "memoryMapVersion": 2,
76
+ "canFD": false,
77
+ "suggestedNode": "velbus-button",
78
+ "subaddresses": []
79
+ }, ...
80
+ ]
81
+ }
82
+ </pre>
83
+
84
+ <h3>Output 2 — Module found</h3>
85
+ <p>One message per discovered module as it responds. Same fields
86
+ as above with <code>topic: "module_found"</code>.</p>
87
+ </script>