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,457 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { pkt, rtrPkt, parsePkt } = require('../../lib/velbus-utils');
|
|
4
|
+
const { DIMMER_TYPES, DIMMER_TYPE_IDS } = require('../../lib/dimmer-types');
|
|
5
|
+
|
|
6
|
+
module.exports = function(RED) {
|
|
7
|
+
|
|
8
|
+
function VelbusDimmerNode(config) {
|
|
9
|
+
RED.nodes.createNode(this, config);
|
|
10
|
+
const node = this;
|
|
11
|
+
|
|
12
|
+
node.bridge = RED.nodes.getNode(config.bridge);
|
|
13
|
+
node.moduleAddr = parseInt(config.moduleAddr, 16);
|
|
14
|
+
node.startChannel = parseInt(config.startChannel) || 1;
|
|
15
|
+
node.channelCount = parseInt(config.channelCount) || 1;
|
|
16
|
+
node.nameOverride = config.name || '';
|
|
17
|
+
|
|
18
|
+
if (!node.bridge) {
|
|
19
|
+
node.error('velbus-dimmer: no bridge configured');
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
if (isNaN(node.moduleAddr) || node.moduleAddr < 1 || node.moduleAddr > 0xFE) {
|
|
23
|
+
node.error('velbus-dimmer: invalid module address');
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// State keyed by channel number (1-N)
|
|
28
|
+
// Original series: dim value is 0-100%
|
|
29
|
+
let _channelState = {};
|
|
30
|
+
// { on, level (0-100), percent (0.0-100.0), relayState, thermal }
|
|
31
|
+
|
|
32
|
+
let _moduleInfo = null;
|
|
33
|
+
let _velbusName = '';
|
|
34
|
+
let _nameParts = {};
|
|
35
|
+
let _blocked = false;
|
|
36
|
+
let _nameTimer = null;
|
|
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
|
+
// Original series: dim value is 0-100 integer percentage
|
|
60
|
+
function decodeModeStatus(modeByte, statusByte) {
|
|
61
|
+
const mode = modeByte & 0x03;
|
|
62
|
+
if (mode === 0x03) return 'forced_off';
|
|
63
|
+
if (mode === 0x02) return 'forced_on';
|
|
64
|
+
if (mode === 0x01) return 'inhibited';
|
|
65
|
+
const s = statusByte & 0x03;
|
|
66
|
+
if (s === 0x03) return 'timer_running';
|
|
67
|
+
if (s === 0x01) return 'on';
|
|
68
|
+
return 'off';
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function decodeThermal(statusByte) {
|
|
72
|
+
// bits 4-5: temp band (0=normal, 1=warm, 2=hot, 3=very hot)
|
|
73
|
+
// bit 3: load type (0=leading edge, 1=trailing edge)
|
|
74
|
+
// bits 1-2: error bits
|
|
75
|
+
return {
|
|
76
|
+
tempBand: (statusByte >> 4) & 0x03,
|
|
77
|
+
loadType: (statusByte >> 3) & 0x01,
|
|
78
|
+
error: (statusByte >> 1) & 0x03
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ── Name retrieval ────────────────────────────────────────────────────
|
|
83
|
+
|
|
84
|
+
function requestName() {
|
|
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-dimmer: module name: ' + name);
|
|
104
|
+
}
|
|
105
|
+
requestStatus();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function requestStatus() {
|
|
109
|
+
// Original series: 0xFA + channel bit
|
|
110
|
+
const typeInfo = _moduleInfo;
|
|
111
|
+
if (typeInfo && typeInfo.channelModel === 'single') {
|
|
112
|
+
node.bridge.send(pkt(0xFB, node.moduleAddr, [0xFA, 0x01]));
|
|
113
|
+
} else {
|
|
114
|
+
// VMB4DC — request each channel
|
|
115
|
+
for (let i = 0; i < node.channelCount; i++) {
|
|
116
|
+
const ch = node.startChannel + i;
|
|
117
|
+
const chBit = 1 << (ch - 1);
|
|
118
|
+
node.bridge.send(pkt(0xFB, node.moduleAddr, [0xFA, chBit]));
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
setStatus(statusPrefix() + ' online', 'grey');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ── 0xFF Firmware check ───────────────────────────────────────────────
|
|
125
|
+
|
|
126
|
+
function handleModuleType(body) {
|
|
127
|
+
const typeId = body[1];
|
|
128
|
+
|
|
129
|
+
if (!DIMMER_TYPE_IDS.has(typeId)) {
|
|
130
|
+
const msg = '⚠ ' + statusPrefix() + ' unknown type ' + addrHex(typeId);
|
|
131
|
+
setStatus(msg, 'red');
|
|
132
|
+
node.error('velbus-dimmer: ' + msg);
|
|
133
|
+
node.send([null, { payload: {
|
|
134
|
+
topic: 'module_unknown',
|
|
135
|
+
address: addrHex(node.moduleAddr),
|
|
136
|
+
typeId: addrHex(typeId),
|
|
137
|
+
message: 'Unrecognised module type — is this the right node? For -20 series use velbus-dimmer-20'
|
|
138
|
+
}}]);
|
|
139
|
+
_blocked = true;
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const typeInfo = DIMMER_TYPES[typeId];
|
|
144
|
+
const serial = body.length >= 4 ? (body[2] << 8 | body[3]) : null;
|
|
145
|
+
const mapVer = body.length >= 5 ? body[4] : null;
|
|
146
|
+
const buildHi = body.length >= 6 ? body[5] : null;
|
|
147
|
+
const buildLo = body.length >= 7 ? body[6] : null;
|
|
148
|
+
const build = (buildHi !== null && buildLo !== null)
|
|
149
|
+
? (buildHi * 100 + buildLo) : null;
|
|
150
|
+
|
|
151
|
+
_moduleInfo = { ...typeInfo, typeId, serial, build, memoryMapVersion: mapVer };
|
|
152
|
+
|
|
153
|
+
const minVer = typeInfo.minMemoryMapVersion;
|
|
154
|
+
if (minVer !== null && mapVer !== null && mapVer < minVer) {
|
|
155
|
+
const msg = '⚠ ' + statusPrefix() + ' firmware too old (map v' + mapVer + ', need v' + minVer + ')';
|
|
156
|
+
setStatus(msg, 'red');
|
|
157
|
+
node.error('velbus-dimmer: ' + msg);
|
|
158
|
+
node.send([null, { payload: {
|
|
159
|
+
topic: 'firmware_incompatible',
|
|
160
|
+
address: addrHex(node.moduleAddr),
|
|
161
|
+
module: typeInfo.name,
|
|
162
|
+
memoryMapVersion: mapVer,
|
|
163
|
+
minimumRequired: minVer,
|
|
164
|
+
message: 'Firmware too old — update module firmware before use'
|
|
165
|
+
}}]);
|
|
166
|
+
_blocked = true;
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (minVer === null) {
|
|
171
|
+
setStatus(statusPrefix() + ' map v' + mapVer + ' (unverified)', 'yellow');
|
|
172
|
+
node.warn('velbus-dimmer: ' + addrHex(node.moduleAddr) + ' ' + typeInfo.name +
|
|
173
|
+
' map v' + mapVer + ' — minimum version unverified, proceeding');
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
_blocked = false;
|
|
177
|
+
|
|
178
|
+
node.send([{ payload: {
|
|
179
|
+
topic: 'module_online',
|
|
180
|
+
address: addrHex(node.moduleAddr),
|
|
181
|
+
module: typeInfo.name,
|
|
182
|
+
typeId: addrHex(typeId),
|
|
183
|
+
channels: typeInfo.channels,
|
|
184
|
+
serial,
|
|
185
|
+
build,
|
|
186
|
+
memoryMapVersion: mapVer
|
|
187
|
+
}}, null]);
|
|
188
|
+
|
|
189
|
+
node.log('velbus-dimmer: ' + typeInfo.name + ' at ' + addrHex(node.moduleAddr) +
|
|
190
|
+
' serial ' + (serial ? serial.toString(16).toUpperCase() : '?') +
|
|
191
|
+
' build ' + build + ' map v' + mapVer);
|
|
192
|
+
|
|
193
|
+
requestName();
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// ── Packet handler ────────────────────────────────────────────────────
|
|
197
|
+
|
|
198
|
+
function onPacket(raw) {
|
|
199
|
+
const p = parsePkt(raw);
|
|
200
|
+
if (!p) return;
|
|
201
|
+
const { body, cmd, rtr } = p;
|
|
202
|
+
|
|
203
|
+
if (cmd === 0xFF && !rtr && body.length >= 2) {
|
|
204
|
+
handleModuleType(body);
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (_blocked) return;
|
|
209
|
+
|
|
210
|
+
if ((cmd === 0xF0 || cmd === 0xF1 || cmd === 0xF2) && body.length >= 2) {
|
|
211
|
+
const part = cmd - 0xEF;
|
|
212
|
+
_nameParts[part] = body.slice(2);
|
|
213
|
+
if (part === 3) assembleName();
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// ── 0xB8 Dimmer status (original series) ─────────────────────────
|
|
218
|
+
// byte 1: cmd (0xB8)
|
|
219
|
+
// byte 2: channel bit (VMBDMI/R: 0x01; VMB4DC: bitmask)
|
|
220
|
+
// byte 3: mode/status byte
|
|
221
|
+
// byte 4: dim value 0-100
|
|
222
|
+
// byte 5: status byte 2 (VMBDMI/R: thermal; VMB4DC: unused)
|
|
223
|
+
// bytes 6-7: timer high/low (16-bit seconds)
|
|
224
|
+
if (cmd === 0xB8 && body.length >= 5) {
|
|
225
|
+
const typeInfo = _moduleInfo;
|
|
226
|
+
const chByte = body[1];
|
|
227
|
+
const modeByte = body[2];
|
|
228
|
+
const dimValue = body[3]; // 0-100
|
|
229
|
+
const statusByte = body[4];
|
|
230
|
+
const timerSec = body.length >= 7 ? ((body[5] << 8) | body[6]) : 0;
|
|
231
|
+
const relayState = decodeModeStatus(modeByte, statusByte);
|
|
232
|
+
|
|
233
|
+
if (typeInfo && typeInfo.channelModel === 'single') {
|
|
234
|
+
// VMBDMI / VMBDMI-R — single channel, chByte always 0x01
|
|
235
|
+
const ch = 1;
|
|
236
|
+
if (ch < node.startChannel || ch >= node.startChannel + node.channelCount) return;
|
|
237
|
+
|
|
238
|
+
const thermal = typeInfo.hasThermal ? decodeThermal(statusByte) : null;
|
|
239
|
+
_channelState[ch] = {
|
|
240
|
+
on: dimValue > 0 && relayState === 'on',
|
|
241
|
+
level: dimValue,
|
|
242
|
+
percent: dimValue * 1.0,
|
|
243
|
+
relayState,
|
|
244
|
+
timerRemaining: timerSec,
|
|
245
|
+
thermal
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
const isWarning = ['forced_on', 'forced_off', 'inhibited'].includes(relayState);
|
|
249
|
+
setStatus(statusPrefix() + ' ' + (dimValue > 0 ? dimValue + '%' : 'off'),
|
|
250
|
+
isWarning ? 'yellow' : (dimValue > 0 ? 'green' : 'grey'));
|
|
251
|
+
|
|
252
|
+
node.send([{ payload: {
|
|
253
|
+
topic: 'dimmer_status',
|
|
254
|
+
address: addrHex(node.moduleAddr),
|
|
255
|
+
module: displayName(),
|
|
256
|
+
channel: ch,
|
|
257
|
+
state: relayState,
|
|
258
|
+
on: dimValue > 0 && relayState === 'on',
|
|
259
|
+
level: dimValue,
|
|
260
|
+
percent: dimValue * 1.0,
|
|
261
|
+
timerRemaining: timerSec,
|
|
262
|
+
thermal,
|
|
263
|
+
timestamp: Date.now()
|
|
264
|
+
}}, isWarning ? { payload: {
|
|
265
|
+
topic: 'dimmer_state_warning',
|
|
266
|
+
address: addrHex(node.moduleAddr),
|
|
267
|
+
module: displayName(),
|
|
268
|
+
channel: ch,
|
|
269
|
+
state: relayState,
|
|
270
|
+
message: 'Channel ' + ch + ' is in ' + relayState
|
|
271
|
+
}} : null]);
|
|
272
|
+
|
|
273
|
+
} else {
|
|
274
|
+
// VMB4DC — chByte is a bitmask
|
|
275
|
+
for (let i = 0; i < 4; i++) {
|
|
276
|
+
const b = 1 << i;
|
|
277
|
+
const ch = i + 1;
|
|
278
|
+
if (!(chByte & b)) continue;
|
|
279
|
+
if (ch < node.startChannel || ch >= node.startChannel + node.channelCount) continue;
|
|
280
|
+
|
|
281
|
+
_channelState[ch] = {
|
|
282
|
+
on: dimValue > 0 && relayState === 'on',
|
|
283
|
+
level: dimValue,
|
|
284
|
+
percent: dimValue * 1.0,
|
|
285
|
+
relayState,
|
|
286
|
+
timerRemaining: timerSec
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
const isWarning = ['forced_on', 'forced_off', 'inhibited'].includes(relayState);
|
|
290
|
+
setStatus(statusPrefix() + ' ch' + ch + ' ' + (dimValue > 0 ? dimValue + '%' : 'off'),
|
|
291
|
+
isWarning ? 'yellow' : (dimValue > 0 ? 'green' : 'grey'));
|
|
292
|
+
|
|
293
|
+
node.send([{ payload: {
|
|
294
|
+
topic: 'dimmer_status',
|
|
295
|
+
address: addrHex(node.moduleAddr),
|
|
296
|
+
module: displayName(),
|
|
297
|
+
channel: ch,
|
|
298
|
+
state: relayState,
|
|
299
|
+
on: dimValue > 0 && relayState === 'on',
|
|
300
|
+
level: dimValue,
|
|
301
|
+
percent: dimValue * 1.0,
|
|
302
|
+
timerRemaining: timerSec,
|
|
303
|
+
timestamp: Date.now()
|
|
304
|
+
}}, isWarning ? { payload: {
|
|
305
|
+
topic: 'dimmer_state_warning',
|
|
306
|
+
address: addrHex(node.moduleAddr),
|
|
307
|
+
module: displayName(),
|
|
308
|
+
channel: ch,
|
|
309
|
+
state: relayState,
|
|
310
|
+
message: 'Channel ' + ch + ' is in ' + relayState
|
|
311
|
+
}} : null]);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// ── Input: command encoder ────────────────────────────────────────────
|
|
318
|
+
|
|
319
|
+
node.on('input', function(msg) {
|
|
320
|
+
if (_blocked) {
|
|
321
|
+
node.warn('velbus-dimmer: commands blocked — firmware incompatible or unknown module');
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const inp = msg.payload;
|
|
326
|
+
if (!inp || typeof inp !== 'object') return;
|
|
327
|
+
|
|
328
|
+
const addr = inp.address !== undefined
|
|
329
|
+
? (typeof inp.address === 'string' ? parseInt(inp.address, 16) : inp.address)
|
|
330
|
+
: node.moduleAddr;
|
|
331
|
+
|
|
332
|
+
const typeInfo = _moduleInfo;
|
|
333
|
+
|
|
334
|
+
// Channel handling:
|
|
335
|
+
// VMBDMI/R: single channel, chBit always 0x01
|
|
336
|
+
// VMB4DC: bitmask, or 0xFF for all
|
|
337
|
+
let chBit;
|
|
338
|
+
if (typeInfo && typeInfo.channelModel === 'single') {
|
|
339
|
+
chBit = 0x01;
|
|
340
|
+
} else if (inp.channel === 'all' || inp.channel === 0 || inp.channel === 255) {
|
|
341
|
+
chBit = 0x0F; // all 4 channels
|
|
342
|
+
} else if (typeof inp.channel === 'number' && inp.channel >= 1 && inp.channel <= 4) {
|
|
343
|
+
chBit = 1 << (inp.channel - 1);
|
|
344
|
+
} else {
|
|
345
|
+
node.warn('velbus-dimmer: missing or invalid channel');
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const cmd = inp.cmd || '';
|
|
350
|
+
const dur = inp.duration || 0;
|
|
351
|
+
const dimspeed = inp.dimspeed || 0; // 16-bit seconds
|
|
352
|
+
|
|
353
|
+
function clampPct(v) {
|
|
354
|
+
return Math.max(0, Math.min(100, Math.round(v)));
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function timer16(sec) {
|
|
358
|
+
if (!sec || sec === 0) return [0, 0];
|
|
359
|
+
if (sec < 0) return [0xFF, 0xFF];
|
|
360
|
+
return [(sec >> 8) & 0xFF, sec & 0xFF];
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const s16 = timer16(dimspeed);
|
|
364
|
+
|
|
365
|
+
function timer24(sec) {
|
|
366
|
+
if (!sec || sec === 0) return [0, 0, 0];
|
|
367
|
+
if (sec < 0) return [0xFF, 0xFF, 0xFF];
|
|
368
|
+
return [(sec >> 16) & 0xFF, (sec >> 8) & 0xFF, sec & 0xFF];
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const t = timer24(dur);
|
|
372
|
+
let packet = null;
|
|
373
|
+
|
|
374
|
+
switch (cmd) {
|
|
375
|
+
case 'set': {
|
|
376
|
+
let level;
|
|
377
|
+
if (typeof inp.level === 'number') {
|
|
378
|
+
level = clampPct(inp.level);
|
|
379
|
+
} else if (typeof inp.percent === 'number') {
|
|
380
|
+
level = clampPct(inp.percent);
|
|
381
|
+
} else {
|
|
382
|
+
node.warn('velbus-dimmer: set command requires level or percent (0-100)');
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
packet = pkt(0xF8, addr, [0x07, chBit, level, ...s16]);
|
|
386
|
+
break;
|
|
387
|
+
}
|
|
388
|
+
case 'on': {
|
|
389
|
+
if (inp.level !== undefined || inp.percent !== undefined) {
|
|
390
|
+
const level = clampPct(inp.level !== undefined ? inp.level : inp.percent);
|
|
391
|
+
packet = pkt(0xF8, addr, [0x07, chBit, level, ...s16]);
|
|
392
|
+
} else {
|
|
393
|
+
packet = pkt(0xF8, addr, [0x11, chBit, ...s16]);
|
|
394
|
+
}
|
|
395
|
+
break;
|
|
396
|
+
}
|
|
397
|
+
case 'off':
|
|
398
|
+
packet = pkt(0xF8, addr, [0x07, chBit, 0x00, ...s16]);
|
|
399
|
+
break;
|
|
400
|
+
case 'restore':
|
|
401
|
+
packet = pkt(0xF8, addr, [0x11, chBit, ...s16]);
|
|
402
|
+
break;
|
|
403
|
+
case 'timer':
|
|
404
|
+
packet = pkt(0xF8, addr, [0x08, chBit, ...t]);
|
|
405
|
+
break;
|
|
406
|
+
case 'forced_on':
|
|
407
|
+
packet = pkt(0xF8, addr, [0x14, chBit, ...t]);
|
|
408
|
+
break;
|
|
409
|
+
case 'forced_off':
|
|
410
|
+
packet = pkt(0xF8, addr, [0x12, chBit, ...t]);
|
|
411
|
+
break;
|
|
412
|
+
case 'cancel_forced_on':
|
|
413
|
+
packet = pkt(0xF8, addr, [0x15, chBit]);
|
|
414
|
+
break;
|
|
415
|
+
case 'cancel_forced_off':
|
|
416
|
+
packet = pkt(0xF8, addr, [0x13, chBit]);
|
|
417
|
+
break;
|
|
418
|
+
case 'inhibit':
|
|
419
|
+
packet = pkt(0xF8, addr, [0x16, chBit, ...t]);
|
|
420
|
+
break;
|
|
421
|
+
case 'cancel_inhibit':
|
|
422
|
+
packet = pkt(0xF8, addr, [0x17, chBit]);
|
|
423
|
+
break;
|
|
424
|
+
case 'stop':
|
|
425
|
+
packet = pkt(0xF8, addr, [0x10, chBit]);
|
|
426
|
+
break;
|
|
427
|
+
case 'status':
|
|
428
|
+
packet = pkt(0xFB, node.moduleAddr, [0xFA, chBit]);
|
|
429
|
+
break;
|
|
430
|
+
default:
|
|
431
|
+
node.warn('velbus-dimmer: unknown command: ' + cmd);
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
if (packet) node.bridge.send(packet);
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
// ── Startup ───────────────────────────────────────────────────────────
|
|
439
|
+
|
|
440
|
+
node.bridge.register(node.moduleAddr, onPacket);
|
|
441
|
+
|
|
442
|
+
setTimeout(() => {
|
|
443
|
+
node.bridge.send(rtrPkt(node.moduleAddr), true);
|
|
444
|
+
}, 500);
|
|
445
|
+
|
|
446
|
+
setStatus('connecting…', 'grey');
|
|
447
|
+
|
|
448
|
+
// ── Cleanup ───────────────────────────────────────────────────────────
|
|
449
|
+
|
|
450
|
+
node.on('close', function() {
|
|
451
|
+
if (_nameTimer) clearTimeout(_nameTimer);
|
|
452
|
+
node.bridge.deregister(node.moduleAddr, onPacket);
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
RED.nodes.registerType('velbus-dimmer', VelbusDimmerNode);
|
|
457
|
+
};
|