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,313 @@
1
+ 'use strict';
2
+
3
+ const { rtrPkt, parsePkt } = require('../../lib/velbus-utils');
4
+ const { GLASS_PANEL_TYPE_IDS } = require('../../lib/glass-panel-types');
5
+
6
+ const ALL_TYPES = {
7
+ // Relays - original
8
+ 0x02: 'VMB1RY', 0x08: 'VMB4RY', 0x10: 'VMB4RYLD',
9
+ 0x11: 'VMB4RYNO', 0x1B: 'VMB1RYNO', 0x29: 'VMB1RYNOS',
10
+ 0x41: 'VMB1RYS', 0x48: 'VMB4RYLD-10', 0x49: 'VMB4RYNO-10',
11
+ // Relays - V2
12
+ 0x0D: 'VMB1RYS-20', 0x26: 'VMB4RYLD-20', 0x27: 'VMB4RYNO-20',
13
+ // Dimmers - original
14
+ 0x15: 'VMBDMI', 0x2F: 'VMBDMI-R', 0x12: 'VMB4DC',
15
+ // Dimmers - V2
16
+ 0x24: 'VMB2DC-20', 0x4B: 'VMB8DC-20', 0x06: 'VMB4LEDPWM-20',
17
+ // Push buttons
18
+ 0x01: 'VMB8PB', 0x16: 'VMB8PBU', 0x17: 'VMB6PBN',
19
+ 0x18: 'VMB2PBN', 0x20: 'VMBGP4', 0x44: 'VMB4PB',
20
+ 0x4C: 'VMB6PB-20',
21
+ // Glass panels - original
22
+ 0x1E: 'VMBGP1', 0x1F: 'VMBGP2', 0x20: 'VMBGP4',
23
+ 0x21: 'VMBGPO', 0x2D: 'VMBGP4PIR',
24
+ 0x34: 'VMBEL1', 0x35: 'VMBEL2', 0x36: 'VMBEL4',
25
+ 0x37: 'VMBELO', 0x38: 'VMBELPIR', 0x39: 'VMBSIG',
26
+ 0x47: 'VMBEL2PIR',
27
+ 0x3A: 'VMBGP1-2', 0x3B: 'VMBGP2-2', 0x3C: 'VMBGP4-2',
28
+ 0x3D: 'VMBGPOD-2',
29
+ // Glass panels - V2
30
+ 0x4F: 'VMBEL1-20', 0x50: 'VMBEL2-20', 0x51: 'VMBEL4-20',
31
+ 0x52: 'VMBELO-20', 0x53: 'VMBELPIR-20', 0x54: 'VMBGP1-20',
32
+ 0x55: 'VMBGP2-20', 0x56: 'VMBGP4-20', 0x57: 'VMBGPO-20',
33
+ 0x5C: 'VMBEL2PIR-20', 0x5F: 'VMBGP4PIR-20',
34
+ // PIR / motion
35
+ 0x23: 'VMBPIRO-10', 0x2A: 'VMBPIRM', 0x2B: 'VMBPIRC',
36
+ 0x2C: 'VMBPIRO', 0x4D: 'VMBPIR-20', 0x59: 'VMBPIRO-20',
37
+ // Sensor / input
38
+ 0x22: 'VMB7IN', 0x32: 'VMB4AN', 0x4E: 'VMB8IN-20',
39
+ // Meteo
40
+ 0x31: 'VMBMETEO',
41
+ // Blind / shutter motor controllers
42
+ 0x03: 'VMB1BL', 0x09: 'VMB2BL',
43
+ 0x2E: 'VMB1BLS', 0x1D: 'VMB2BLE', 0x4A: 'VMB2BLE-10', 0x61: 'VMB2BLE-20',
44
+ // Blind / shutter (old labels were wrong — VMB2BLE is NOT Bluetooth)
45
+ // Power/energy
46
+ 0x04: 'VMBPSUMNGR-20',
47
+ // Interface
48
+ 0x40: 'VMBUSBIP',
49
+ // Temperature sensor (old)
50
+ 0x0C: 'VMB1TS',
51
+ };
52
+
53
+ const NODE_SUGGESTION = {
54
+ // Relays - original
55
+ 0x02: 'velbus-relay', 0x08: 'velbus-relay', 0x10: 'velbus-relay',
56
+ 0x11: 'velbus-relay', 0x1B: 'velbus-relay', 0x29: 'velbus-relay',
57
+ 0x41: 'velbus-relay', 0x48: 'velbus-relay', 0x49: 'velbus-relay',
58
+ // Relays - V2
59
+ 0x0D: 'velbus-relay-20', 0x26: 'velbus-relay-20', 0x27: 'velbus-relay-20',
60
+ // Dimmers - original
61
+ 0x15: 'velbus-dimmer', 0x2F: 'velbus-dimmer', 0x12: 'velbus-dimmer',
62
+ // Dimmers - V2
63
+ 0x24: 'velbus-dimmer-20', 0x4B: 'velbus-dimmer-20', 0x06: 'velbus-dimmer-20',
64
+ // Button modules
65
+ 0x01: 'velbus-button', 0x16: 'velbus-button', 0x17: 'velbus-button',
66
+ 0x18: 'velbus-button', 0x44: 'velbus-button', 0x4C: 'velbus-button',
67
+ // Blind / shutter (confirmed PDFs read)
68
+ 0x03: 'velbus-blind', 0x09: 'velbus-blind',
69
+ 0x1D: 'velbus-blind-s', 0x2E: 'velbus-blind-s',
70
+ 0x4A: 'velbus-blind-s', 0x61: 'velbus-blind-20',
71
+ // Sensor / input
72
+ 0x22: 'velbus-sensor', 0x32: 'velbus-sensor', 0x4E: 'velbus-sensor-20',
73
+ // Meteo
74
+ 0x31: 'velbus-meteo',
75
+ // PIR
76
+ 0x23: 'velbus-pir', 0x2A: 'velbus-pir', 0x2B: 'velbus-pir',
77
+ 0x2C: 'velbus-pir', 0x4D: 'velbus-pir-20', 0x59: 'velbus-pir-20',
78
+ // Glass panels - all via single node
79
+ 0x1E: 'velbus-glass-panel', 0x1F: 'velbus-glass-panel', 0x20: 'velbus-glass-panel',
80
+ 0x21: 'velbus-glass-panel', 0x2D: 'velbus-glass-panel',
81
+ 0x34: 'velbus-glass-panel', 0x35: 'velbus-glass-panel', 0x36: 'velbus-glass-panel',
82
+ 0x37: 'velbus-glass-panel', 0x38: 'velbus-glass-panel', 0x47: 'velbus-glass-panel',
83
+ 0x3A: 'velbus-glass-panel', 0x3B: 'velbus-glass-panel', 0x3C: 'velbus-glass-panel',
84
+ 0x3D: 'velbus-glass-panel',
85
+ 0x4F: 'velbus-glass-panel', 0x50: 'velbus-glass-panel', 0x51: 'velbus-glass-panel',
86
+ 0x52: 'velbus-glass-panel', 0x53: 'velbus-glass-panel', 0x54: 'velbus-glass-panel',
87
+ 0x55: 'velbus-glass-panel', 0x56: 'velbus-glass-panel', 0x57: 'velbus-glass-panel',
88
+ 0x5C: 'velbus-glass-panel', 0x5F: 'velbus-glass-panel',
89
+ };
90
+
91
+ // Channel count per module type — physical channels only.
92
+ // Virtual channels (conditional logic, lockouts) are not included;
93
+ // installers set those manually.
94
+ const MODULE_CHANNELS = {
95
+ // Relays - original
96
+ 0x02: 1, 0x08: 4, 0x10: 4,
97
+ 0x11: 4, 0x1B: 1, 0x29: 1,
98
+ 0x41: 1, 0x48: 4, 0x49: 4,
99
+ // Relays - V2
100
+ 0x0D: 1, 0x26: 4, 0x27: 4,
101
+ // Dimmers - original
102
+ 0x15: 1, 0x2F: 1, 0x12: 4,
103
+ // Dimmers - V2
104
+ 0x24: 2, 0x4B: 8, 0x06: 4,
105
+ // Push buttons
106
+ 0x01: 8, 0x16: 8, 0x17: 6, 0x18: 2,
107
+ 0x44: 4, 0x4C: 6,
108
+ // PIR
109
+ 0x23: 6, 0x2A: 7, 0x2B: 7, 0x2C: 6, 0x4D: 7, 0x59: 6,
110
+ // Blind / shutter (all PDFs confirmed)
111
+ 0x03: 1, 0x09: 2, 0x1D: 2, 0x2E: 1, 0x4A: 2, 0x61: 2,
112
+ // Sensor / input
113
+ 0x22: 8, 0x31: 8, 0x32: 16, 0x4E: 32,
114
+ // Glass panels - original
115
+ 0x1E: 1, 0x1F: 2, 0x20: 4,
116
+ 0x21: 32, 0x2D: 8,
117
+ 0x34: 1, 0x35: 2, 0x36: 4,
118
+ 0x37: 4, 0x38: 8, 0x47: 6,
119
+ 0x3A: 1, 0x3B: 2, 0x3C: 4,
120
+ 0x3D: 4,
121
+ // Glass panels - V2
122
+ 0x4F: 1, 0x50: 2, 0x51: 4,
123
+ 0x52: 4, 0x53: 8, 0x54: 1,
124
+ 0x55: 2, 0x56: 4, 0x57: 32,
125
+ 0x5C: 6, 0x5F: 8,
126
+ };
127
+
128
+ module.exports = function(RED) {
129
+
130
+ function VelbusScanNode(config) {
131
+ RED.nodes.createNode(this, config);
132
+ const node = this;
133
+
134
+ node.bridge = RED.nodes.getNode(config.bridge);
135
+ node.rtrDelay = parseInt(config.rtrDelay) || 75; // ms between RTR packets
136
+ node.collectTime = parseInt(config.collectTime) || 8000; // ms to collect after last RTR
137
+
138
+ if (!node.bridge) {
139
+ node.error('velbus-scan: no bridge configured');
140
+ return;
141
+ }
142
+
143
+ let _scanning = false;
144
+ let _discovered = {};
145
+ let _collectTimer = null;
146
+
147
+ function addrHex(a) {
148
+ return '0x' + a.toString(16).padStart(2, '0').toUpperCase();
149
+ }
150
+
151
+ // ── Packet handler — registered as 'all' listener ─────────────────────
152
+
153
+ function onPacket(raw) {
154
+ if (!_scanning) return;
155
+
156
+ const p = parsePkt(raw);
157
+ if (!p || p.rtr) return;
158
+ const { addr, body, cmd } = p;
159
+
160
+ // 0xFF module type response
161
+ if (cmd === 0xFF && body.length >= 2) {
162
+ if (_discovered[addr]) return; // already found this address
163
+
164
+ const typeId = body[1];
165
+ const serial = body.length >= 4 ? (body[2] << 8 | body[3]) : null;
166
+ const mapVer = body.length >= 5 ? body[4] : null;
167
+ const buildHi = body.length >= 6 ? body[5] : null;
168
+ const buildLo = body.length >= 7 ? body[6] : null;
169
+ const build = (buildHi !== null && buildLo !== null)
170
+ ? (buildHi * 100 + buildLo) : null;
171
+ const props = body.length >= 8 ? body[7] : null;
172
+ const canFD = props !== null ? !!(props & 0x20) : false;
173
+
174
+ const typeName = ALL_TYPES[typeId] || ('unknown_' + addrHex(typeId));
175
+ const suggestedNode = NODE_SUGGESTION[typeId] || null;
176
+ const channels = MODULE_CHANNELS[typeId] || null;
177
+
178
+ _discovered[addr] = {
179
+ address: addrHex(addr),
180
+ addressDec: addr,
181
+ typeId: addrHex(typeId),
182
+ module: typeName,
183
+ serial: serial ? serial.toString(16).toUpperCase().padStart(4, '0') : null,
184
+ build,
185
+ memoryMapVersion: mapVer,
186
+ canFD,
187
+ suggestedNode,
188
+ channels,
189
+ subaddresses: []
190
+ };
191
+
192
+ node.log('velbus-scan: found ' + typeName + ' at ' + addrHex(addr));
193
+ node.send([null, { payload: { topic: 'module_found', ..._discovered[addr] } }]);
194
+ return;
195
+ }
196
+
197
+ // 0xB0 subtype — merge subaddresses into already-discovered module
198
+ if (cmd === 0xB0 && body.length >= 8) {
199
+ const module = _discovered[addr];
200
+ if (module) {
201
+ module.subaddresses = [body[4], body[5], body[6], body[7]]
202
+ .filter(s => s !== 0xFF && s >= 0x01 && s <= 0xFE)
203
+ .map(s => addrHex(s));
204
+ }
205
+ }
206
+ }
207
+
208
+ // ── Scan ──────────────────────────────────────────────────────────────
209
+
210
+ function startScan() {
211
+ if (_scanning) {
212
+ node.warn('velbus-scan: scan already in progress');
213
+ return;
214
+ }
215
+
216
+ _scanning = true;
217
+ _discovered = {};
218
+
219
+ // Lock bridge — queues interpreter startup RTRs until scan completes
220
+ if (node.bridge.lockScan) node.bridge.lockScan();
221
+
222
+ node.bridge.register('all', onPacket);
223
+ node.status({ fill: 'blue', shape: 'dot', text: 'scanning…' });
224
+
225
+ // Fire RTRs one at a time using recursive setTimeout
226
+ // Avoids scheduling 254 timers simultaneously on low-memory hardware
227
+ const addresses = [];
228
+ for (let a = 0xFE; a >= 0x01; a--) addresses.push(a);
229
+ let rtrIndex = 0;
230
+
231
+ function sendNextRTR() {
232
+ if (!_scanning || rtrIndex >= addresses.length) {
233
+ // All RTRs sent — start collect window
234
+ node.status({ fill: 'blue', shape: 'dot', text: 'collecting responses…' });
235
+ _collectTimer = setTimeout(finaliseScan, node.collectTime);
236
+ return;
237
+ }
238
+
239
+ const addr = addresses[rtrIndex];
240
+ const pct = Math.round((rtrIndex / addresses.length) * 100);
241
+
242
+ if (rtrIndex % 32 === 0) {
243
+ node.status({ fill: 'blue', shape: 'dot',
244
+ text: 'scanning ' + addrHex(addr) + ' (' + pct + '%)' });
245
+ }
246
+
247
+ node.bridge.send(rtrPkt(addr));
248
+ rtrIndex++;
249
+ setTimeout(sendNextRTR, node.rtrDelay);
250
+ }
251
+
252
+ sendNextRTR();
253
+ }
254
+
255
+ function finaliseScan() {
256
+ if (_collectTimer) { clearTimeout(_collectTimer); _collectTimer = null; }
257
+ _scanning = false;
258
+
259
+ node.bridge.deregister('all', onPacket);
260
+ if (node.bridge.unlockScan) node.bridge.unlockScan();
261
+
262
+ const modules = Object.values(_discovered)
263
+ .sort((a, b) => a.addressDec - b.addressDec);
264
+ const count = modules.length;
265
+
266
+ node.log('velbus-scan: finalising — ' + count + ' modules found');
267
+
268
+ node.status({ fill: 'green', shape: 'dot',
269
+ text: count + ' module' + (count !== 1 ? 's' : '') + ' found' });
270
+
271
+ const payload = { topic: 'scan_complete', modules, count };
272
+ node.log('velbus-scan: sending scan_complete payload');
273
+ node.send([{ payload }, null]);
274
+ node.log('velbus-scan: send complete');
275
+
276
+ // Store results on bridge for config dialog dropdowns
277
+ if (node.bridge.storeScanResults) {
278
+ node.bridge.storeScanResults(modules);
279
+ }
280
+
281
+ node.log('velbus-scan: complete — ' + count + ' modules: ' +
282
+ modules.map(m => m.address + ' ' + m.module).join(', '));
283
+ }
284
+
285
+ // ── Input ─────────────────────────────────────────────────────────────
286
+
287
+ node.on('input', function(msg) {
288
+ const cmd = (msg.payload && msg.payload.cmd) || msg.payload || 'scan';
289
+ if (cmd === 'scan' || cmd === true || cmd === 1) {
290
+ startScan();
291
+ } else if (cmd === 'cancel') {
292
+ if (_scanning) {
293
+ if (_collectTimer) { clearTimeout(_collectTimer); _collectTimer = null; }
294
+ node.bridge.deregister('all', onPacket);
295
+ if (node.bridge.unlockScan) node.bridge.unlockScan();
296
+ _scanning = false;
297
+ node.status({ fill: 'grey', shape: 'dot', text: 'cancelled' });
298
+ }
299
+ }
300
+ });
301
+
302
+ // ── Cleanup ───────────────────────────────────────────────────────────
303
+
304
+ node.on('close', function() {
305
+ if (_collectTimer) clearTimeout(_collectTimer);
306
+ node.bridge.deregister('all', onPacket);
307
+ });
308
+
309
+ node.status({ fill: 'grey', shape: 'dot', text: 'ready — inject to scan' });
310
+ }
311
+
312
+ RED.nodes.registerType('velbus-scan', VelbusScanNode);
313
+ };
@@ -0,0 +1,154 @@
1
+ <script type="text/javascript">
2
+ (function() {
3
+
4
+ const SENSOR_TYPES = {
5
+ 0x22: { name: 'VMB7IN', hasCounter: true, series: 'original' },
6
+ 0x32: { name: 'VMB4AN', hasCounter: false, series: 'original' },
7
+ };
8
+
9
+ RED.nodes.registerType('velbus-sensor', {
10
+ category: 'Velbus (inputs)',
11
+ color: '#3A8C8C',
12
+ defaults: {
13
+ name: { value: '' },
14
+ bridge: { value: '', type: 'velbus-bridge', required: true },
15
+ address: { value: '', required: true, validate: function(v) { return /^(0x)?[0-9a-fA-F]{1,2}$/.test(v); } },
16
+ moduleName: { value: '' },
17
+ typeId: { value: '' },
18
+ },
19
+ inputs: 1,
20
+ outputs: 2,
21
+ outputLabels: ['channel events / status', 'counter data'],
22
+ icon: 'font-awesome/fa-tachometer',
23
+ paletteLabel: 'sensor',
24
+
25
+ label: function() {
26
+ const t = this.typeId ? SENSOR_TYPES[parseInt(this.typeId, 16)] : null;
27
+ const n = this.name || this.moduleName || (t ? t.name : 'sensor');
28
+ return n + ' 0x' + (parseInt(this.address) || 0).toString(16).padStart(2,'0').toUpperCase();
29
+ },
30
+
31
+ oneditprepare: function() {
32
+ const savedAddr = this.address;
33
+ $('#node-input-typeId').prop('readonly', true).css('background', '#f0f0f0');
34
+
35
+ function populateDropdown(bridgeId) {
36
+ if (!bridgeId) return;
37
+ $.getJSON('velbus/scan-results?bridge=' + bridgeId, function(results) {
38
+ const modules = (results && results.modules) ? results.modules : [];
39
+ const $addr = $('#node-input-address');
40
+ $addr.empty().append($('<option>').val('').text('— select address —'));
41
+
42
+ const typeIds = Object.keys(SENSOR_TYPES).map(k => parseInt(k));
43
+ const filtered = modules.filter(m => typeIds.includes(parseInt(m.typeId, 16)));
44
+
45
+ if (!filtered.length) {
46
+ $addr.append($('<option>').val('').text('No sensor modules found on bus'));
47
+ return;
48
+ }
49
+ filtered.forEach(function(m) {
50
+ const tid = parseInt(m.typeId, 16);
51
+ const label = m.address + ' — ' + m.module;
52
+ const $opt = $('<option>').val(m.addressDec).text(label).data('typeid', tid);
53
+ if (m.addressDec === parseInt(savedAddr)) $opt.attr('selected', true);
54
+ $addr.append($opt);
55
+ });
56
+ });
57
+ }
58
+
59
+ function updateHint(addrDec) {
60
+ const $opt = $('#node-input-address option[value="' + addrDec + '"]');
61
+ const tid = $opt.data('typeid');
62
+ if (tid && SENSOR_TYPES[tid]) {
63
+ const t = SENSOR_TYPES[tid];
64
+ $('#sensor-type-hint').text(t.name + (t.hasCounter ? ' · pulse counter ch1-4' : '')).show();
65
+ $('#node-input-typeId').val('0x' + tid.toString(16).padStart(2,'0').toUpperCase());
66
+ } else {
67
+ $('#sensor-type-hint').hide();
68
+ }
69
+ }
70
+
71
+ $('#node-input-bridge').on('change', function() { populateDropdown($(this).val()); });
72
+ $('#node-input-address').on('change', function() { updateHint(parseInt($(this).val())); });
73
+ // Deferred load — TypedInput sets value async after oneditprepare
74
+
75
+ $('#node-input-bridge').trigger('change');
76
+
77
+ setTimeout(function() { $('#node-input-bridge').trigger('change'); }, 150);
78
+ },
79
+
80
+ oneditsave: function() {
81
+ this.address = parseInt($('#node-input-address').val()) || 0;
82
+ const tid = $('#node-input-typeId').val();
83
+ this.typeId = tid || '';
84
+ if (tid) {
85
+ const t = SENSOR_TYPES[parseInt(tid, 16)];
86
+ if (t) this.moduleName = t.name;
87
+ }
88
+ },
89
+ });
90
+
91
+ })();
92
+ </script>
93
+
94
+ <script type="text/html" data-template-name="velbus-sensor">
95
+ <div class="form-row">
96
+ <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
97
+ <input type="text" id="node-input-name" placeholder="Optional label">
98
+ </div>
99
+ <div class="form-row">
100
+ <label for="node-input-bridge"><i class="fa fa-plug"></i> Bridge</label>
101
+ <input type="text" id="node-input-bridge">
102
+ </div>
103
+ <div class="form-row">
104
+ <label for="node-input-address"><i class="fa fa-map-marker"></i> Address</label>
105
+ <select id="node-input-address" style="width:70%">
106
+ <option value="">— select bridge first —</option>
107
+ </select>
108
+ </div>
109
+ <div class="form-row">
110
+ <label></label>
111
+ <span id="sensor-type-hint" style="color:#888; font-size:0.9em; display:none;"></span>
112
+ </div>
113
+ <div class="form-row" style="display:none">
114
+ <input type="text" id="node-input-typeId">
115
+ <input type="text" id="node-input-moduleName">
116
+ </div>
117
+ </script>
118
+
119
+ <script type="text/html" data-help-name="velbus-sensor">
120
+ <p style="background:#fff3cd; border-left:4px solid #ffc107; padding:8px 12px; margin-bottom:12px; font-size:0.85em;">
121
+ <strong>&#9888; Testing status:</strong> This node was generated with Claude.ai and is in
122
+ need of extensive field testing before being deployed into a commercial project.
123
+ It is presented "as is" &mdash; any use beyond testing is entirely at your own risk and liability.
124
+ Constructive feedback is welcomed, accompanied by as many examples and debug captures as possible.
125
+ <a href="https://github.com/MDAR/node-red-contrib-velbus-2026/issues" target="_blank">File an issue on GitHub</a>.
126
+ </p>
127
+ <p>Universal input node for Velbus original series sensor/input modules.</p>
128
+ <p>Supported: VMB7IN (0x22), VMB4AN (0x32).</p>
129
+ <p>For V2 series use <strong>velbus-sensor-20</strong>.</p>
130
+
131
+ <h3>Output 1 — Channel events / status</h3>
132
+ <p>Channel event (0x00):</p>
133
+ <pre>{ "type": "channel", "on": true, "pressed": [1,3], "released": [], "longPressed": [] }</pre>
134
+ <p>Module status (0xED):</p>
135
+ <pre>{ "type": "status", "on": true, "channels": [1,3],
136
+ "enabled": [1,2,3,4,5,6,7,8], "locked": [], "progDisabled": [],
137
+ "program": "summer", "alarms": { "alarm1Active": false, "alarm2Active": false } }</pre>
138
+
139
+ <h3>Output 2 — Counter data (VMB7IN only)</h3>
140
+ <pre>{ "type": "counter", "channel": 1, "count": 12345,
141
+ "pulsesPerUnit": 1000, "periodMs": 3600, "overflow": false }</pre>
142
+ <p>Engineering units: calculate in flow using pulsesPerUnit and periodMs.</p>
143
+ <p>Power (W) = 3,600,000,000 / (periodMs × pulsesPerUnit)</p>
144
+
145
+ <h3>Input commands</h3>
146
+ <dl>
147
+ <dt><code>{ "cmd": "get_status" }</code></dt><dd>Request module status.</dd>
148
+ <dt><code>{ "cmd": "get_counter", "channels": 15, "interval": 30 }</code></dt>
149
+ <dd>Request counter (VMB7IN). channels=bitmask of ch1-4 (default 15=all). interval=0 no change.</dd>
150
+ <dt><code>{ "cmd": "reset_counter", "channel": 1 }</code></dt><dd>Reset pulse counter to zero.</dd>
151
+ <dt><code>{ "cmd": "load_counter", "channel": 1, "value": 12345 }</code></dt><dd>Load counter with preset value (build 1426+).</dd>
152
+ <dt><code>{ "cmd": "get_name", "channel": 1 }</code></dt><dd>Request channel name from VelbusLink.</dd>
153
+ </dl>
154
+ </script>