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,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
|
+
<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
|
+
<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>⚠ 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" — 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>
|