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.
- package/CHANGELOG_FORUM.md +784 -0
- package/LICENSE +21 -0
- package/README.md +140 -0
- package/examples/velbus-basic-relay-dimmer.json +328 -0
- package/index.js +3 -0
- package/lib/blind-types-20.js +26 -0
- package/lib/blind-types-s.js +46 -0
- package/lib/blind-types.js +38 -0
- package/lib/dimmer-types-20.js +120 -0
- package/lib/dimmer-types.js +34 -0
- package/lib/glass-panel-types.js +296 -0
- package/lib/pir-types-20.js +51 -0
- package/lib/pir-types.js +61 -0
- package/lib/relay-types-20.js +43 -0
- package/lib/relay-types.js +39 -0
- package/lib/sensor-types-20.js +29 -0
- package/lib/sensor-types.js +49 -0
- package/lib/velbus-utils.js +80 -0
- package/nodes/velbus-blind/velbus-blind.html +165 -0
- package/nodes/velbus-blind/velbus-blind.js +251 -0
- package/nodes/velbus-blind-20/velbus-blind-20.html +179 -0
- package/nodes/velbus-blind-20/velbus-blind-20.js +300 -0
- package/nodes/velbus-blind-s/velbus-blind-s.html +181 -0
- package/nodes/velbus-blind-s/velbus-blind-s.js +282 -0
- package/nodes/velbus-bridge/velbus-bridge.html +72 -0
- package/nodes/velbus-bridge/velbus-bridge.js +304 -0
- package/nodes/velbus-button/velbus-button.html +172 -0
- package/nodes/velbus-button/velbus-button.js +117 -0
- package/nodes/velbus-clock/velbus-clock.html +198 -0
- package/nodes/velbus-clock/velbus-clock.js +273 -0
- package/nodes/velbus-dimmer/velbus-dimmer.html +174 -0
- package/nodes/velbus-dimmer/velbus-dimmer.js +457 -0
- package/nodes/velbus-dimmer-20/velbus-dimmer-20.html +271 -0
- package/nodes/velbus-dimmer-20/velbus-dimmer-20.js +602 -0
- package/nodes/velbus-glass-panel/velbus-glass-panel.html +337 -0
- package/nodes/velbus-glass-panel/velbus-glass-panel.js +492 -0
- package/nodes/velbus-meteo/velbus-meteo.html +120 -0
- package/nodes/velbus-meteo/velbus-meteo.js +279 -0
- package/nodes/velbus-pir/velbus-pir.html +187 -0
- package/nodes/velbus-pir/velbus-pir.js +269 -0
- package/nodes/velbus-pir-20/velbus-pir-20.html +186 -0
- package/nodes/velbus-pir-20/velbus-pir-20.js +352 -0
- package/nodes/velbus-relay/velbus-relay.html +215 -0
- package/nodes/velbus-relay/velbus-relay.js +404 -0
- package/nodes/velbus-relay-20/velbus-relay-20.html +123 -0
- package/nodes/velbus-relay-20/velbus-relay-20.js +434 -0
- package/nodes/velbus-scan/velbus-scan.html +87 -0
- package/nodes/velbus-scan/velbus-scan.js +313 -0
- package/nodes/velbus-sensor/velbus-sensor.html +154 -0
- package/nodes/velbus-sensor/velbus-sensor.js +259 -0
- package/nodes/velbus-sensor-20/velbus-sensor-20.html +158 -0
- package/nodes/velbus-sensor-20/velbus-sensor-20.js +314 -0
- package/nodes/velbus-thermostat/velbus-thermostat.html +191 -0
- package/nodes/velbus-thermostat/velbus-thermostat.js +322 -0
- package/package.json +55 -0
- 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>⚠ 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" — 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>
|