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,404 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { pkt, rtrPkt, parsePkt } = require('../../lib/velbus-utils');
|
|
4
|
+
const { RELAY_TYPES, RELAY_TYPE_IDS } = require('../../lib/relay-types');
|
|
5
|
+
|
|
6
|
+
module.exports = function(RED) {
|
|
7
|
+
|
|
8
|
+
function VelbusRelayNode(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 || ''; // Node-RED label override
|
|
18
|
+
|
|
19
|
+
if (!node.bridge) {
|
|
20
|
+
node.error('velbus-relay: no bridge configured');
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
if (isNaN(node.moduleAddr) || node.moduleAddr < 1 || node.moduleAddr > 0xFE) {
|
|
24
|
+
node.error('velbus-relay: invalid module address');
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// State
|
|
29
|
+
let _channelState = {}; // { [chBit]: { relayState, ledState, timerRemaining } }
|
|
30
|
+
let _moduleInfo = null; // populated on 0xFF
|
|
31
|
+
let _velbusName = ''; // name retrieved from hardware via 0xEF
|
|
32
|
+
let _nameParts = {}; // { 1: chars1-6, 2: chars7-12, 3: chars13-16 }
|
|
33
|
+
let _blocked = false; // true if firmware check failed — hard block
|
|
34
|
+
let _nameTimer = null;
|
|
35
|
+
|
|
36
|
+
// ── Helpers ───────────────────────────────────────────────────────────
|
|
37
|
+
|
|
38
|
+
function addrHex(a) {
|
|
39
|
+
return '0x' + a.toString(16).padStart(2, '0').toUpperCase();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function displayName() {
|
|
43
|
+
if (node.nameOverride) return node.nameOverride;
|
|
44
|
+
if (_velbusName) return _velbusName;
|
|
45
|
+
if (_moduleInfo) return _moduleInfo.name;
|
|
46
|
+
return addrHex(node.moduleAddr);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function statusPrefix() {
|
|
50
|
+
return displayName() + ' (' + addrHex(node.moduleAddr) + ')';
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function setStatus(text, fill, shape) {
|
|
54
|
+
node.status({ fill: fill || 'grey', shape: shape || 'dot', text });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function modeStr(mode, status) {
|
|
58
|
+
const m = mode & 0x03;
|
|
59
|
+
if (m === 0x03) return 'forced_off';
|
|
60
|
+
if (m === 0x02) return 'forced_on';
|
|
61
|
+
if (m === 0x01) return 'inhibited';
|
|
62
|
+
const s = status & 0x03;
|
|
63
|
+
if (s === 0x03) return 'timer_running';
|
|
64
|
+
if (s === 0x01) return 'on';
|
|
65
|
+
return 'off';
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function decodeLed(ledByte) {
|
|
69
|
+
if (ledByte & 0x80) return 'on';
|
|
70
|
+
if (ledByte & 0x40) return 'slow_blink';
|
|
71
|
+
if (ledByte & 0x20) return 'fast_blink';
|
|
72
|
+
if (ledByte & 0x10) return 'very_fast_blink';
|
|
73
|
+
return 'off';
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function buildStateMsg(chBit, state) {
|
|
77
|
+
return {
|
|
78
|
+
topic: 'relay_status',
|
|
79
|
+
address: addrHex(node.moduleAddr),
|
|
80
|
+
module: displayName(),
|
|
81
|
+
channel: Math.log2(chBit) + 1,
|
|
82
|
+
channelBit: addrHex(chBit),
|
|
83
|
+
state: state.relayState,
|
|
84
|
+
on: state.relayState === 'on' || state.relayState === 'timer_running' || state.relayState === 'forced_on',
|
|
85
|
+
timerRemaining: state.timerRemaining || 0,
|
|
86
|
+
ledState: state.ledState || 'off',
|
|
87
|
+
timestamp: Date.now()
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ── Name retrieval ────────────────────────────────────────────────────
|
|
92
|
+
|
|
93
|
+
function requestName() {
|
|
94
|
+
// Send 0xEF with channel 0xFF = module name request
|
|
95
|
+
// Priority 0xF8 (command) not 0xFB (status) — critical
|
|
96
|
+
const p = pkt(0xF8, node.moduleAddr, [0xEF, 0xFF]);
|
|
97
|
+
node.bridge.send(p);
|
|
98
|
+
// Timeout — use whatever was received after 2s
|
|
99
|
+
_nameTimer = setTimeout(assembleName, 2000);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function assembleName() {
|
|
103
|
+
if (_nameTimer) { clearTimeout(_nameTimer); _nameTimer = null; }
|
|
104
|
+
const raw = [
|
|
105
|
+
...(_nameParts[1] || []),
|
|
106
|
+
...(_nameParts[2] || []),
|
|
107
|
+
...(_nameParts[3] || [])
|
|
108
|
+
];
|
|
109
|
+
const name = raw
|
|
110
|
+
.filter(c => c !== 0xFF && c !== 0x00)
|
|
111
|
+
.map(c => String.fromCharCode(c))
|
|
112
|
+
.join('')
|
|
113
|
+
.trim();
|
|
114
|
+
if (name) {
|
|
115
|
+
_velbusName = name;
|
|
116
|
+
node.log('velbus-relay: module name retrieved: ' + name);
|
|
117
|
+
}
|
|
118
|
+
// Now request status
|
|
119
|
+
requestStatus();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function requestStatus() {
|
|
123
|
+
for (let i = 0; i < node.channelCount; i++) {
|
|
124
|
+
const ch = node.startChannel + i;
|
|
125
|
+
const chBit = 1 << (ch - 1);
|
|
126
|
+
node.bridge.send(pkt(0xFB, node.moduleAddr, [0xFA, chBit]));
|
|
127
|
+
}
|
|
128
|
+
setStatus(statusPrefix() + ' online', 'grey');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ── 0xFF Firmware check ───────────────────────────────────────────────
|
|
132
|
+
|
|
133
|
+
function handleModuleType(body) {
|
|
134
|
+
const typeId = body[1];
|
|
135
|
+
|
|
136
|
+
// Stage 1: type recognised?
|
|
137
|
+
if (!RELAY_TYPE_IDS.has(typeId)) {
|
|
138
|
+
const msg = '⚠ ' + statusPrefix() + ' unknown type ' + addrHex(typeId);
|
|
139
|
+
setStatus(msg, 'red');
|
|
140
|
+
node.error('velbus-relay: ' + msg);
|
|
141
|
+
node.send([null, { payload: {
|
|
142
|
+
topic: 'module_unknown',
|
|
143
|
+
address: addrHex(node.moduleAddr),
|
|
144
|
+
typeId: addrHex(typeId),
|
|
145
|
+
message: 'Unrecognised module type — is this the right node for this module?'
|
|
146
|
+
}}]);
|
|
147
|
+
_blocked = true;
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const typeInfo = RELAY_TYPES[typeId];
|
|
152
|
+
const serial = body.length >= 4 ? (body[2] << 8 | body[3]) : null;
|
|
153
|
+
const mapVer = body.length >= 5 ? body[4] : null;
|
|
154
|
+
const buildHi = body.length >= 6 ? body[5] : null;
|
|
155
|
+
const buildLo = body.length >= 7 ? body[6] : null;
|
|
156
|
+
const build = (buildHi !== null && buildLo !== null)
|
|
157
|
+
? (buildHi * 100 + buildLo) : null;
|
|
158
|
+
|
|
159
|
+
_moduleInfo = { ...typeInfo, typeId, serial, build, memoryMapVersion: mapVer };
|
|
160
|
+
|
|
161
|
+
// Stage 2: memory map version check
|
|
162
|
+
const minVer = typeInfo.minMemoryMapVersion;
|
|
163
|
+
if (minVer !== null && mapVer !== null && mapVer < minVer) {
|
|
164
|
+
const msg = '⚠ ' + statusPrefix() + ' firmware too old (map v' + mapVer + ', need v' + minVer + ')';
|
|
165
|
+
setStatus(msg, 'red');
|
|
166
|
+
node.error('velbus-relay: ' + msg);
|
|
167
|
+
node.send([null, { payload: {
|
|
168
|
+
topic: 'firmware_incompatible',
|
|
169
|
+
address: addrHex(node.moduleAddr),
|
|
170
|
+
module: typeInfo.name,
|
|
171
|
+
memoryMapVersion: mapVer,
|
|
172
|
+
minimumRequired: minVer,
|
|
173
|
+
message: 'Firmware too old — update module firmware before use'
|
|
174
|
+
}}]);
|
|
175
|
+
_blocked = true;
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Stage 2b: unverified (null minimum)
|
|
180
|
+
if (minVer === null) {
|
|
181
|
+
setStatus(statusPrefix() + ' map v' + mapVer + ' (unverified)', 'yellow');
|
|
182
|
+
node.warn('velbus-relay: ' + addrHex(node.moduleAddr) + ' ' + typeInfo.name +
|
|
183
|
+
' map v' + mapVer + ' — minimum version unverified, proceeding');
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Stage 3: pass — emit module_online, retrieve name
|
|
187
|
+
_blocked = false;
|
|
188
|
+
|
|
189
|
+
node.send([{ payload: {
|
|
190
|
+
topic: 'module_online',
|
|
191
|
+
address: addrHex(node.moduleAddr),
|
|
192
|
+
module: typeInfo.name,
|
|
193
|
+
typeId: addrHex(typeId),
|
|
194
|
+
serial,
|
|
195
|
+
build,
|
|
196
|
+
memoryMapVersion: mapVer
|
|
197
|
+
}}, null]);
|
|
198
|
+
|
|
199
|
+
node.log('velbus-relay: ' + typeInfo.name + ' at ' + addrHex(node.moduleAddr) +
|
|
200
|
+
' serial ' + (serial ? serial.toString(16).toUpperCase() : '?') +
|
|
201
|
+
' build ' + build + ' map v' + mapVer);
|
|
202
|
+
|
|
203
|
+
requestName();
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// ── Packet handler ────────────────────────────────────────────────────
|
|
207
|
+
|
|
208
|
+
function onPacket(raw) {
|
|
209
|
+
const p = parsePkt(raw);
|
|
210
|
+
if (!p) return;
|
|
211
|
+
const { pri, body, cmd, rtr } = p;
|
|
212
|
+
|
|
213
|
+
// ── 0xFF Module type ─────────────────────────────────────────────────
|
|
214
|
+
if (cmd === 0xFF && !rtr && body.length >= 2) {
|
|
215
|
+
handleModuleType(body);
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// All remaining packets blocked if firmware check failed
|
|
220
|
+
if (_blocked) return;
|
|
221
|
+
|
|
222
|
+
// ── 0xF0/0xF1/0xF2 Channel name parts ───────────────────────────────
|
|
223
|
+
if ((cmd === 0xF0 || cmd === 0xF1 || cmd === 0xF2) && body.length >= 2) {
|
|
224
|
+
const part = cmd - 0xEF; // 0xF0=1, 0xF1=2, 0xF2=3
|
|
225
|
+
_nameParts[part] = body.slice(2); // skip cmd + channel bytes
|
|
226
|
+
if (part === 3) assembleName();
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// ── 0xFB Relay status (original series per-channel format) ───────────
|
|
231
|
+
if (cmd === 0xFB && body.length >= 8) {
|
|
232
|
+
const chBit = body[1];
|
|
233
|
+
const modeVal = body[2];
|
|
234
|
+
const statusVal = body[3];
|
|
235
|
+
const ledByte = body[4];
|
|
236
|
+
const timerSec = (body[5] << 16) | (body[6] << 8) | body[7];
|
|
237
|
+
const relayState = modeStr(modeVal, statusVal);
|
|
238
|
+
const ledState = decodeLed(ledByte);
|
|
239
|
+
|
|
240
|
+
const ch = Math.log2(chBit) + 1;
|
|
241
|
+
if (ch < node.startChannel || ch >= node.startChannel + node.channelCount) return;
|
|
242
|
+
|
|
243
|
+
_channelState[chBit] = { relayState, ledState, timerRemaining: timerSec };
|
|
244
|
+
|
|
245
|
+
const stateMsg = buildStateMsg(chBit, _channelState[chBit]);
|
|
246
|
+
let warnMsg = null;
|
|
247
|
+
|
|
248
|
+
if (['forced_on', 'forced_off', 'inhibited'].includes(relayState)) {
|
|
249
|
+
warnMsg = {
|
|
250
|
+
topic: 'relay_state_warning',
|
|
251
|
+
address: addrHex(node.moduleAddr),
|
|
252
|
+
module: displayName(),
|
|
253
|
+
channel: ch,
|
|
254
|
+
state: relayState,
|
|
255
|
+
message: 'Channel ' + ch + ' is in ' + relayState + ' — direct commands will be ignored by module'
|
|
256
|
+
};
|
|
257
|
+
setStatus(statusPrefix() + ' ch' + ch + ' ' + relayState, 'yellow');
|
|
258
|
+
} else {
|
|
259
|
+
setStatus(statusPrefix() + ' ch' + ch + ' ' + relayState,
|
|
260
|
+
relayState === 'on' ? 'green' : 'grey');
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
node.send([{ payload: stateMsg }, warnMsg ? { payload: warnMsg } : null]);
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// ── 0x00 Channel switched broadcast ──────────────────────────────────
|
|
268
|
+
// Fires on every pulse edge during interval_timer blinking, and on
|
|
269
|
+
// every local push-button-driven switch. Confirmed against
|
|
270
|
+
// protocol_vmb4ryld_10.pdf: SID10-SID9=00 = highest priority = 0xF8
|
|
271
|
+
// (see packetprotocol README priority table). The previous `pri ===
|
|
272
|
+
// 0xFB` check required the opposite (lowest priority) and silently
|
|
273
|
+
// dropped every one of these broadcasts — nothing else in this file
|
|
274
|
+
// gates on priority, and this cmd byte has only one defined meaning
|
|
275
|
+
// for a relay module, so no filter is needed at all.
|
|
276
|
+
if (cmd === 0x00 && body.length >= 4) {
|
|
277
|
+
const onBits = body[1];
|
|
278
|
+
const offBits = body[2];
|
|
279
|
+
|
|
280
|
+
for (let i = 0; i < 8; i++) {
|
|
281
|
+
const b = 1 << i;
|
|
282
|
+
const ch = i + 1;
|
|
283
|
+
if (ch < node.startChannel || ch >= node.startChannel + node.channelCount) continue;
|
|
284
|
+
|
|
285
|
+
if (onBits & b) {
|
|
286
|
+
node.send([{ payload: {
|
|
287
|
+
topic: 'relay_switched', address: addrHex(node.moduleAddr),
|
|
288
|
+
module: displayName(), channel: ch, channelBit: addrHex(b), state: 'on'
|
|
289
|
+
}}, null]);
|
|
290
|
+
}
|
|
291
|
+
if (offBits & b) {
|
|
292
|
+
node.send([{ payload: {
|
|
293
|
+
topic: 'relay_switched', address: addrHex(node.moduleAddr),
|
|
294
|
+
module: displayName(), channel: ch, channelBit: addrHex(b), state: 'off'
|
|
295
|
+
}}, null]);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// ── Input: command encoder ────────────────────────────────────────────
|
|
302
|
+
|
|
303
|
+
node.on('input', function(msg) {
|
|
304
|
+
if (_blocked) {
|
|
305
|
+
node.warn('velbus-relay: commands blocked — firmware incompatible or unknown module');
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const inp = msg.payload;
|
|
310
|
+
if (!inp || typeof inp !== 'object') return;
|
|
311
|
+
|
|
312
|
+
const addr = inp.address !== undefined
|
|
313
|
+
? (typeof inp.address === 'string' ? parseInt(inp.address, 16) : inp.address)
|
|
314
|
+
: node.moduleAddr;
|
|
315
|
+
|
|
316
|
+
let chBit;
|
|
317
|
+
if (typeof inp.channel === 'number' && inp.channel >= 1 && inp.channel <= 8) {
|
|
318
|
+
chBit = 1 << (inp.channel - 1);
|
|
319
|
+
} else if (typeof inp.channelBit === 'number') {
|
|
320
|
+
chBit = inp.channelBit;
|
|
321
|
+
} else {
|
|
322
|
+
node.warn('velbus-relay: missing channel in command');
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const cmd = inp.cmd || '';
|
|
327
|
+
const dur = inp.duration || 0;
|
|
328
|
+
|
|
329
|
+
const cs = _channelState[chBit];
|
|
330
|
+
if (cs && ['forced_on', 'forced_off', 'inhibited'].includes(cs.relayState)) {
|
|
331
|
+
const cancelCmds = ['cancel_forced_on', 'cancel_forced_off', 'cancel_inhibit',
|
|
332
|
+
'forced_on', 'forced_off', 'inhibit'];
|
|
333
|
+
if (!cancelCmds.includes(cmd)) {
|
|
334
|
+
node.warn('velbus-relay ' + addrHex(addr) + ' ch' + inp.channel +
|
|
335
|
+
': channel is in ' + cs.relayState + ' — command may be ignored by module');
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function timer24(sec) {
|
|
340
|
+
if (!sec || sec === 0) return [0, 0, 0];
|
|
341
|
+
if (sec < 0) return [0xFF, 0xFF, 0xFF];
|
|
342
|
+
return [(sec >> 16) & 0xFF, (sec >> 8) & 0xFF, sec & 0xFF];
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const t = timer24(dur);
|
|
346
|
+
let packet = null;
|
|
347
|
+
|
|
348
|
+
switch (cmd) {
|
|
349
|
+
case 'on': packet = pkt(0xF8, addr, [0x02, chBit]); break;
|
|
350
|
+
case 'off': packet = pkt(0xF8, addr, [0x01, chBit]); break;
|
|
351
|
+
case 'toggle': {
|
|
352
|
+
const isOn = cs && cs.relayState === 'on';
|
|
353
|
+
packet = pkt(0xF8, addr, [isOn ? 0x01 : 0x02, chBit]);
|
|
354
|
+
break;
|
|
355
|
+
}
|
|
356
|
+
case 'timer': packet = pkt(0xF8, addr, [0x03, chBit, ...t]); break;
|
|
357
|
+
case 'interval_timer': {
|
|
358
|
+
// 'Start relay blinking timer' (0x0D) — confirmed against the official
|
|
359
|
+
// protocol PDFs for every module this node covers (VMB4RYLD/-10,
|
|
360
|
+
// VMB4RYNO/-10, VMB1RYNO, VMB1RYNOS, VMB1RYS): DLC=5, body is
|
|
361
|
+
// [0x0D, chBit, 24-bit delay time] — ONE time parameter, not three.
|
|
362
|
+
// There is no live bus command for a configurable pulse/pause rate —
|
|
363
|
+
// the module blinks at its own fixed rate for the given duration.
|
|
364
|
+
// duration: 0=skip (module ignores), -1 or 0xFFFFFF=permanent blinking.
|
|
365
|
+
packet = pkt(0xF8, addr, [0x0D, chBit, ...t]);
|
|
366
|
+
break;
|
|
367
|
+
}
|
|
368
|
+
case 'forced_on': packet = pkt(0xF8, addr, [0x14, chBit, ...t]); break;
|
|
369
|
+
case 'forced_off': packet = pkt(0xF8, addr, [0x12, chBit, ...t]); break;
|
|
370
|
+
case 'cancel_forced_on': packet = pkt(0xF8, addr, [0x15, chBit]); break;
|
|
371
|
+
case 'cancel_forced_off': packet = pkt(0xF8, addr, [0x13, chBit]); break;
|
|
372
|
+
case 'inhibit': packet = pkt(0xF8, addr, [0x16, chBit, ...t]); break;
|
|
373
|
+
case 'cancel_inhibit': packet = pkt(0xF8, addr, [0x17, chBit]); break;
|
|
374
|
+
case 'status': packet = pkt(0xFB, addr, [0xFA, chBit]); break;
|
|
375
|
+
default:
|
|
376
|
+
node.warn('velbus-relay: unknown command: ' + cmd);
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
if (packet) node.bridge.send(packet);
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
// ── Startup ───────────────────────────────────────────────────────────
|
|
384
|
+
|
|
385
|
+
node.bridge.register(node.moduleAddr, onPacket);
|
|
386
|
+
|
|
387
|
+
// Send RTR scan after short delay — triggers 0xFF and 0xB0 from module
|
|
388
|
+
// startup=true flags this as a startup packet — will be queued if scan in progress
|
|
389
|
+
setTimeout(() => {
|
|
390
|
+
node.bridge.send(rtrPkt(node.moduleAddr), true);
|
|
391
|
+
}, 500);
|
|
392
|
+
|
|
393
|
+
setStatus('connecting…', 'grey');
|
|
394
|
+
|
|
395
|
+
// ── Cleanup ───────────────────────────────────────────────────────────
|
|
396
|
+
|
|
397
|
+
node.on('close', function() {
|
|
398
|
+
if (_nameTimer) clearTimeout(_nameTimer);
|
|
399
|
+
node.bridge.deregister(node.moduleAddr, onPacket);
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
RED.nodes.registerType('velbus-relay', VelbusRelayNode);
|
|
404
|
+
};
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
<script type="text/javascript">
|
|
2
|
+
RED.nodes.registerType('velbus-relay-20', {
|
|
3
|
+
category: 'Velbus (outputs)',
|
|
4
|
+
color: '#4A90D9',
|
|
5
|
+
defaults: {
|
|
6
|
+
name: { value: '' },
|
|
7
|
+
bridge: { value: '', type: 'velbus-bridge', required: true },
|
|
8
|
+
moduleAddr: { value: '', required: true },
|
|
9
|
+
startChannel: { value: 1, required: true, validate: RED.validators.number() },
|
|
10
|
+
channelCount: { value: 4, required: true, validate: RED.validators.number() }
|
|
11
|
+
},
|
|
12
|
+
inputs: 1,
|
|
13
|
+
outputs: 2,
|
|
14
|
+
icon: 'font-awesome/fa-toggle-on',
|
|
15
|
+
label: function() {
|
|
16
|
+
if (this.name) return this.name;
|
|
17
|
+
return 'relay-20 0x' + (this.moduleAddr || '??');
|
|
18
|
+
},
|
|
19
|
+
outputLabels: ['state / events', 'warnings'],
|
|
20
|
+
oneditprepare: function() {
|
|
21
|
+
velbusPopulateAddressDropdown(
|
|
22
|
+
this,
|
|
23
|
+
'#node-input-bridge',
|
|
24
|
+
'#node-input-moduleAddr',
|
|
25
|
+
['velbus-relay-20']
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
</script>
|
|
30
|
+
|
|
31
|
+
<script type="text/html" data-template-name="velbus-relay-20">
|
|
32
|
+
<div class="form-row">
|
|
33
|
+
<label for="node-input-name"><i class="fa fa-tag"></i> Name override</label>
|
|
34
|
+
<input type="text" id="node-input-name"
|
|
35
|
+
placeholder="Leave blank to use module name from VelbusLink">
|
|
36
|
+
</div>
|
|
37
|
+
<div class="form-row">
|
|
38
|
+
<label for="node-input-bridge"><i class="fa fa-server"></i> Bridge</label>
|
|
39
|
+
<input type="text" id="node-input-bridge">
|
|
40
|
+
</div>
|
|
41
|
+
<div class="form-row">
|
|
42
|
+
<label for="node-input-moduleAddr"><i class="fa fa-microchip"></i> Address</label>
|
|
43
|
+
<input type="text" id="node-input-moduleAddr" placeholder="e.g. B9 — run scan first for dropdown" style="width:100%">
|
|
44
|
+
<div class="form-tips" style="margin-top:4px">Run a <b>velbus-scan</b> node first to populate the dropdown with discovered modules.</div>
|
|
45
|
+
</div>
|
|
46
|
+
<div class="form-row">
|
|
47
|
+
<label for="node-input-startChannel"><i class="fa fa-list-ol"></i> Start channel</label>
|
|
48
|
+
<input type="number" id="node-input-startChannel" min="1" max="8" style="width:60px">
|
|
49
|
+
</div>
|
|
50
|
+
<div class="form-row">
|
|
51
|
+
<label for="node-input-channelCount"><i class="fa fa-hashtag"></i> Channel count</label>
|
|
52
|
+
<input type="number" id="node-input-channelCount" min="1" max="8" style="width:60px">
|
|
53
|
+
</div>
|
|
54
|
+
</script>
|
|
55
|
+
|
|
56
|
+
<script type="text/html" data-help-name="velbus-relay-20">
|
|
57
|
+
<p style="background:#fff3cd; border-left:4px solid #ffc107; padding:8px 12px; margin-bottom:12px; font-size:0.85em;">
|
|
58
|
+
<strong>⚠ Testing status:</strong> This node was generated with Claude.ai and is in
|
|
59
|
+
need of extensive field testing before being deployed into a commercial project.
|
|
60
|
+
It is presented "as is" — any use beyond testing is entirely at your own risk and liability.
|
|
61
|
+
Constructive feedback is welcomed, accompanied by as many examples and debug captures as possible.
|
|
62
|
+
<a href="https://github.com/MDAR/node-red-contrib-velbus-2026/issues" target="_blank">File an issue on GitHub</a>.
|
|
63
|
+
</p>
|
|
64
|
+
<p><strong>V2.0 series only (-20 suffix modules)</strong></p>
|
|
65
|
+
<p>For original series relay modules use the <b>velbus-relay</b> node instead.</p>
|
|
66
|
+
|
|
67
|
+
<p>Supports: <b>VMB4RYLD-20</b>, <b>VMB4RYNO-20</b>, <b>VMB1RYS-20</b></p>
|
|
68
|
+
|
|
69
|
+
<h3>Address dropdown</h3>
|
|
70
|
+
<p>Run a <b>velbus-scan</b> node first to populate the address dropdown.</p>
|
|
71
|
+
|
|
72
|
+
<h3>Output 1 — State / Events</h3>
|
|
73
|
+
<pre>
|
|
74
|
+
{
|
|
75
|
+
"topic": "relay_status",
|
|
76
|
+
"address": "0xB9",
|
|
77
|
+
"module": "Garage",
|
|
78
|
+
"channel": 1,
|
|
79
|
+
"state": "on",
|
|
80
|
+
"on": true,
|
|
81
|
+
"inhibited": false,
|
|
82
|
+
"forcedOn": false,
|
|
83
|
+
"forcedOff": false,
|
|
84
|
+
"programDisabled":false,
|
|
85
|
+
"timerRunning": false,
|
|
86
|
+
"alarmProgram": { "program":0, "alarm1":false, ... },
|
|
87
|
+
"timestamp": 1719100000000
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// relay_switched — fires on every local push-button-driven switch. Separate
|
|
91
|
+
// from relay_status above, which reports the overall channel mode rather
|
|
92
|
+
// than individual switch edges.
|
|
93
|
+
{
|
|
94
|
+
"topic": "relay_switched",
|
|
95
|
+
"address": "0xB9",
|
|
96
|
+
"module": "Garage",
|
|
97
|
+
"channel": 1,
|
|
98
|
+
"state": "on" // or "off"
|
|
99
|
+
}
|
|
100
|
+
</pre>
|
|
101
|
+
|
|
102
|
+
<h3>Input — Commands</h3>
|
|
103
|
+
<pre>
|
|
104
|
+
{ "channel": 1, "cmd": "on" }
|
|
105
|
+
{ "channel": "all", "cmd": "off" }
|
|
106
|
+
{ "channel": 1, "cmd": "toggle" }
|
|
107
|
+
{ "channel": 1, "cmd": "timer", "duration": 60 }
|
|
108
|
+
{ "channel": 1, "cmd": "forced_on", "duration": -1 }
|
|
109
|
+
{ "channel": 1, "cmd": "forced_off", "duration": 3600 }
|
|
110
|
+
{ "channel": 1, "cmd": "cancel_forced_on" }
|
|
111
|
+
{ "channel": 1, "cmd": "cancel_forced_off" }
|
|
112
|
+
{ "channel": 1, "cmd": "inhibit", "duration": 1800 }
|
|
113
|
+
{ "channel": 1, "cmd": "cancel_inhibit" }
|
|
114
|
+
{ "channel": 1, "cmd": "status" }
|
|
115
|
+
</pre>
|
|
116
|
+
<p>Set <code>channel</code> to <code>"all"</code> to send to all channels simultaneously.</p>
|
|
117
|
+
<p>Duration in seconds. Use <code>-1</code> for permanent.</p>
|
|
118
|
+
<p><b>No interval/blink timer command.</b> Unlike the original series
|
|
119
|
+
(<b>velbus-relay</b>), V2 relays have no live bus command for a blink/interval
|
|
120
|
+
timer. It can only be achieved by writing a Program Step (Action code 22) to
|
|
121
|
+
module memory, linked to a button or scenario — a commissioning task, not
|
|
122
|
+
a live command this node sends.</p>
|
|
123
|
+
</script>
|