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,259 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { pkt, parsePkt } = require('../../lib/velbus-utils');
|
|
4
|
+
const { SENSOR_TYPES, SENSOR_TYPE_IDS } = require('../../lib/sensor-types');
|
|
5
|
+
|
|
6
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
7
|
+
// Helpers
|
|
8
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
function activeFromBitmask(byte) {
|
|
11
|
+
const active = [];
|
|
12
|
+
for (let i = 0; i < 8; i++) {
|
|
13
|
+
if (byte & (1 << i)) active.push(i + 1);
|
|
14
|
+
}
|
|
15
|
+
return active;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function decodeNameBitmask(byte) {
|
|
19
|
+
// Returns the channel number (1-8) for a single-bit bitmask
|
|
20
|
+
for (let i = 0; i < 8; i++) {
|
|
21
|
+
if (byte === (1 << i)) return i + 1;
|
|
22
|
+
}
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
27
|
+
// Node
|
|
28
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
module.exports = function(RED) {
|
|
31
|
+
|
|
32
|
+
function VelbusSensorNode(config) {
|
|
33
|
+
RED.nodes.createNode(this, config);
|
|
34
|
+
const node = this;
|
|
35
|
+
|
|
36
|
+
node.bridge = RED.nodes.getNode(config.bridge);
|
|
37
|
+
node.address = typeof config.address === 'number'
|
|
38
|
+
? config.address // legacy saves stored decimal numbers
|
|
39
|
+
: (parseInt(config.address, 16) || 0); // editor stores hex strings
|
|
40
|
+
node.moduleName = config.moduleName || '';
|
|
41
|
+
node.typeId = config.typeId ? parseInt(config.typeId) : null;
|
|
42
|
+
|
|
43
|
+
if (!node.bridge) {
|
|
44
|
+
node.status({ fill: 'red', shape: 'ring', text: 'no bridge' });
|
|
45
|
+
node.error('velbus-sensor: no bridge configured');
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (!node.address || node.address < 1 || node.address > 254) {
|
|
49
|
+
node.status({ fill: 'red', shape: 'ring', text: 'invalid address' });
|
|
50
|
+
node.error('velbus-sensor: invalid address ' + node.address);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const typeDesc = node.typeId !== null ? (SENSOR_TYPES[node.typeId] || null) : null;
|
|
55
|
+
const hasCounter = typeDesc ? typeDesc.hasCounter : false;
|
|
56
|
+
const isVMB4AN = node.typeId === 0x32;
|
|
57
|
+
|
|
58
|
+
const _addrHex = '0x' + node.address.toString(16).padStart(2, '0').toUpperCase();
|
|
59
|
+
const _label = () => node.moduleName
|
|
60
|
+
? node.moduleName + ' (' + _addrHex + ')'
|
|
61
|
+
: (typeDesc ? typeDesc.name : 'sensor') + ' ' + _addrHex;
|
|
62
|
+
|
|
63
|
+
function setStatus(text, fill, shape) {
|
|
64
|
+
node.status({ fill: fill || 'green', shape: shape || 'dot',
|
|
65
|
+
text: _label() + ' ' + text });
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Channel name store — keyed by channel number (1-8)
|
|
69
|
+
const _channelNames = {};
|
|
70
|
+
const _nameParts = {}; // keyed by channel number, then part 0/1/2
|
|
71
|
+
|
|
72
|
+
function assembleName(ch) {
|
|
73
|
+
const parts = _nameParts[ch];
|
|
74
|
+
if (!parts) return;
|
|
75
|
+
const bytes = [
|
|
76
|
+
...(parts[0] || []),
|
|
77
|
+
...(parts[1] || []),
|
|
78
|
+
...(parts[2] || []),
|
|
79
|
+
].filter(b => b !== 0 && b !== 0xFF);
|
|
80
|
+
const name = String.fromCharCode(...bytes).trim();
|
|
81
|
+
if (name) _channelNames[ch] = name;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ── Packet handler ────────────────────────────────────────────────────
|
|
85
|
+
|
|
86
|
+
function onPacket(raw) {
|
|
87
|
+
const p = parsePkt(raw);
|
|
88
|
+
if (!p || p.rtr) return;
|
|
89
|
+
// Note: no address re-check here. The bridge routes frames to this
|
|
90
|
+
// listener, including subaddress frames mapped to the primary address
|
|
91
|
+
// (e.g. GPO-20 thermostat status from subaddress 0x34).
|
|
92
|
+
|
|
93
|
+
const { cmd, body } = p;
|
|
94
|
+
|
|
95
|
+
switch (cmd) {
|
|
96
|
+
|
|
97
|
+
// ── 0x00 Channel events ──────────────────────────────────────────
|
|
98
|
+
case 0x00: {
|
|
99
|
+
if (body.length < 4) return;
|
|
100
|
+
// body[0]=cmd, body[1]=pressed, body[2]=released, body[3]=longPressed
|
|
101
|
+
const pressed = activeFromBitmask(body[1]);
|
|
102
|
+
const released = activeFromBitmask(body[2]);
|
|
103
|
+
const longPressed = activeFromBitmask(body[3]);
|
|
104
|
+
const on = pressed.length > 0 || longPressed.length > 0;
|
|
105
|
+
|
|
106
|
+
const payload = { type: 'channel', on, pressed, released, longPressed };
|
|
107
|
+
if (pressed.length) setStatus('ch' + pressed.join(',') + ' on');
|
|
108
|
+
node.send([{ payload }, null]);
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ── 0xED Module status ───────────────────────────────────────────
|
|
113
|
+
case 0xED: {
|
|
114
|
+
// VMB7IN: PDF says DLC=5 but lists 7 fields — treat as 7 data bytes
|
|
115
|
+
if (body.length < 7) return;
|
|
116
|
+
// body[0]=0xED, body[1]=channel status, body[2]=enabled/disabled,
|
|
117
|
+
// body[3]=normal/inverted, body[4]=locked, body[5]=prog-disabled,
|
|
118
|
+
// body[6]=alarm&program
|
|
119
|
+
const on = body[1] !== 0;
|
|
120
|
+
const progByte = body[6];
|
|
121
|
+
const program = ['none', 'summer', 'winter', 'holiday'][progByte & 0x03];
|
|
122
|
+
const alarm1Active = !!(progByte & 0x04);
|
|
123
|
+
const alarm2Active = !!(progByte & 0x10);
|
|
124
|
+
const sunriseEnabled = !!(progByte & 0x40);
|
|
125
|
+
const sunsetEnabled = !!(progByte & 0x80);
|
|
126
|
+
|
|
127
|
+
const payload = {
|
|
128
|
+
type: 'status',
|
|
129
|
+
on,
|
|
130
|
+
channels: activeFromBitmask(body[1]),
|
|
131
|
+
enabled: activeFromBitmask(body[2]),
|
|
132
|
+
normal: activeFromBitmask(body[3]),
|
|
133
|
+
locked: activeFromBitmask(body[4]),
|
|
134
|
+
progDisabled: activeFromBitmask(body[5]),
|
|
135
|
+
program,
|
|
136
|
+
alarms: { alarm1Active, alarm2Active },
|
|
137
|
+
sunrise: sunriseEnabled,
|
|
138
|
+
sunset: sunsetEnabled,
|
|
139
|
+
};
|
|
140
|
+
node.send([{ payload }, null]);
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ── 0xBE Pulse counter status (VMB7IN channels 1-4) ─────────────
|
|
145
|
+
case 0xBE: {
|
|
146
|
+
if (!hasCounter) return;
|
|
147
|
+
if (body.length < 8) return;
|
|
148
|
+
// body[0]=0xBE
|
|
149
|
+
// body[1]: bits0-1=channel(0-3), bits2-7=pulses-per-unit scaling
|
|
150
|
+
// body[2-5]: 32-bit pulse count (MSB first)
|
|
151
|
+
// body[6-7]: 16-bit period in ms between last 2 pulses (0xFFFF=overflow)
|
|
152
|
+
const channelIndex = body[1] & 0x03; // 0-3
|
|
153
|
+
const channel = channelIndex + 1; // 1-4
|
|
154
|
+
const scalingBits = (body[1] >> 2) & 0x3F; // bits 2-7
|
|
155
|
+
// scaling encodes pulses per unit * 100 (e.g. 0x01=100, 0x0A=1000)
|
|
156
|
+
const pulsesPerUnit = scalingBits * 100;
|
|
157
|
+
|
|
158
|
+
const count = ((body[2] << 24) | (body[3] << 16) | (body[4] << 8) | body[5]) >>> 0;
|
|
159
|
+
const periodMs = (body[6] << 8) | body[7];
|
|
160
|
+
const overflow = periodMs === 0xFFFF;
|
|
161
|
+
|
|
162
|
+
const payload = {
|
|
163
|
+
type: 'counter',
|
|
164
|
+
channel,
|
|
165
|
+
count,
|
|
166
|
+
pulsesPerUnit,
|
|
167
|
+
periodMs: overflow ? null : periodMs,
|
|
168
|
+
overflow,
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
node.send([null, { payload }]);
|
|
172
|
+
break;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ── 0xF0/F1/F2 Channel name parts ───────────────────────────────
|
|
176
|
+
case 0xF0:
|
|
177
|
+
case 0xF1:
|
|
178
|
+
case 0xF2: {
|
|
179
|
+
// body[0]=cmd, body[1]=channel bitmask, body[2..]=chars
|
|
180
|
+
const mask = body[1];
|
|
181
|
+
const ch = decodeNameBitmask(mask);
|
|
182
|
+
if (!ch) break;
|
|
183
|
+
const part = cmd - 0xF0;
|
|
184
|
+
if (!_nameParts[ch]) _nameParts[ch] = {};
|
|
185
|
+
_nameParts[ch][part] = Array.from(body).slice(2);
|
|
186
|
+
if (part === 2) assembleName(ch);
|
|
187
|
+
break;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
default:
|
|
191
|
+
break;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
node.bridge.register(node.address, onPacket);
|
|
196
|
+
|
|
197
|
+
// ── Input commands ────────────────────────────────────────────────────
|
|
198
|
+
|
|
199
|
+
node.on('input', function(msg) {
|
|
200
|
+
const cmd = msg.payload && msg.payload.cmd;
|
|
201
|
+
if (!cmd) return;
|
|
202
|
+
|
|
203
|
+
if (cmd === 'get_status') {
|
|
204
|
+
node.bridge.send(pkt(0xFB, node.address, [0xFA, 0x00]));
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Request counter status — DB2=channel bitmask (bits 0-3 = ch 1-4), DB3=interval
|
|
209
|
+
if (cmd === 'get_counter') {
|
|
210
|
+
if (!hasCounter) { node.warn('velbus-sensor: this module has no pulse counter'); return; }
|
|
211
|
+
const chMask = parseInt(msg.payload.channels) || 0x0F; // default all 4
|
|
212
|
+
const interval = parseInt(msg.payload.interval) || 0;
|
|
213
|
+
node.bridge.send(pkt(0xF8, node.address, [0xBD, chMask, interval]));
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Reset counter — DB2=channel number (0-3)
|
|
218
|
+
if (cmd === 'reset_counter') {
|
|
219
|
+
if (!hasCounter) { node.warn('velbus-sensor: this module has no pulse counter'); return; }
|
|
220
|
+
const ch = (parseInt(msg.payload.channel) || 1) - 1; // 0-indexed
|
|
221
|
+
node.bridge.send(pkt(0xF8, node.address, [0xAD, ch]));
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Load counter — DB2=channel number (0-3), DB3=don't care, DB4-7=32-bit value
|
|
226
|
+
if (cmd === 'load_counter') {
|
|
227
|
+
if (!hasCounter) { node.warn('velbus-sensor: this module has no pulse counter'); return; }
|
|
228
|
+
const ch = (parseInt(msg.payload.channel) || 1) - 1;
|
|
229
|
+
const value = parseInt(msg.payload.value) || 0;
|
|
230
|
+
const b4 = (value >>> 24) & 0xFF;
|
|
231
|
+
const b5 = (value >>> 16) & 0xFF;
|
|
232
|
+
const b6 = (value >>> 8) & 0xFF;
|
|
233
|
+
const b7 = value & 0xFF;
|
|
234
|
+
node.bridge.send(pkt(0xF8, node.address, [0xAD, ch, 0x00, b4, b5, b6, b7]));
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Request channel name — DB2=channel bitmask
|
|
239
|
+
if (cmd === 'get_name') {
|
|
240
|
+
const ch = parseInt(msg.payload.channel) || 0;
|
|
241
|
+
const mask = ch >= 1 && ch <= 8 ? (1 << (ch - 1)) : 0xFF;
|
|
242
|
+
node.bridge.send(pkt(0xF8, node.address, [0xEF, mask]));
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
node.warn('velbus-sensor: unknown cmd: ' + cmd);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
// ── Cleanup ───────────────────────────────────────────────────────────
|
|
250
|
+
|
|
251
|
+
node.on('close', function() {
|
|
252
|
+
node.bridge.deregister(node.address, onPacket);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
setStatus('ready', 'grey', 'dot');
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
RED.nodes.registerType('velbus-sensor', VelbusSensorNode);
|
|
259
|
+
};
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
<script type="text/javascript">
|
|
2
|
+
(function() {
|
|
3
|
+
|
|
4
|
+
const SENSOR_TYPES_20 = {
|
|
5
|
+
0x4E: { name: 'VMB8IN-20', hasCounter: true, series: 'v2' },
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
RED.nodes.registerType('velbus-sensor-20', {
|
|
9
|
+
category: 'Velbus (inputs)',
|
|
10
|
+
color: '#3A8C8C',
|
|
11
|
+
defaults: {
|
|
12
|
+
name: { value: '' },
|
|
13
|
+
bridge: { value: '', type: 'velbus-bridge', required: true },
|
|
14
|
+
address: { value: '', required: true, validate: function(v) { return /^(0x)?[0-9a-fA-F]{1,2}$/.test(v); } },
|
|
15
|
+
moduleName: { value: '' },
|
|
16
|
+
typeId: { value: '' },
|
|
17
|
+
},
|
|
18
|
+
inputs: 1,
|
|
19
|
+
outputs: 2,
|
|
20
|
+
outputLabels: ['channel events / status', 'energy counter'],
|
|
21
|
+
icon: 'font-awesome/fa-tachometer',
|
|
22
|
+
paletteLabel: 'sensor-20',
|
|
23
|
+
|
|
24
|
+
label: function() {
|
|
25
|
+
const t = this.typeId ? SENSOR_TYPES_20[parseInt(this.typeId, 16)] : null;
|
|
26
|
+
const n = this.name || this.moduleName || (t ? t.name : 'sensor-20');
|
|
27
|
+
return n + ' 0x' + (parseInt(this.address) || 0).toString(16).padStart(2,'0').toUpperCase();
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
oneditprepare: function() {
|
|
31
|
+
const savedAddr = this.address;
|
|
32
|
+
$('#node-input-typeId').prop('readonly', true).css('background', '#f0f0f0');
|
|
33
|
+
|
|
34
|
+
function populateDropdown(bridgeId) {
|
|
35
|
+
if (!bridgeId) return;
|
|
36
|
+
$.getJSON('velbus/scan-results?bridge=' + bridgeId, function(results) {
|
|
37
|
+
const modules = (results && results.modules) ? results.modules : [];
|
|
38
|
+
const $addr = $('#node-input-address');
|
|
39
|
+
$addr.empty().append($('<option>').val('').text('— select address —'));
|
|
40
|
+
|
|
41
|
+
const typeIds = Object.keys(SENSOR_TYPES_20).map(k => parseInt(k));
|
|
42
|
+
const filtered = modules.filter(m => typeIds.includes(parseInt(m.typeId, 16)));
|
|
43
|
+
|
|
44
|
+
if (!filtered.length) {
|
|
45
|
+
$addr.append($('<option>').val('').text('No V2 sensor modules found on bus'));
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
filtered.forEach(function(m) {
|
|
49
|
+
const tid = parseInt(m.typeId, 16);
|
|
50
|
+
const label = m.address + ' — ' + m.module;
|
|
51
|
+
const $opt = $('<option>').val(m.addressDec).text(label).data('typeid', tid);
|
|
52
|
+
if (m.addressDec === parseInt(savedAddr)) $opt.attr('selected', true);
|
|
53
|
+
$addr.append($opt);
|
|
54
|
+
});
|
|
55
|
+
if (savedAddr) {
|
|
56
|
+
const $opt = $('#node-input-address option[value="' + parseInt(savedAddr) + '"]');
|
|
57
|
+
const tid = $opt.data('typeid');
|
|
58
|
+
if (tid) {
|
|
59
|
+
$('#sensor20-type-hint').text(SENSOR_TYPES_20[tid].name + ' · up to 32ch · energy counters').show();
|
|
60
|
+
$('#node-input-typeId').val('0x' + tid.toString(16).padStart(2,'0').toUpperCase());
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
$('#node-input-bridge').on('change', function() { populateDropdown($(this).val()); });
|
|
67
|
+
$('#node-input-address').on('change', function() {
|
|
68
|
+
const tid = $(this).find(':selected').data('typeid');
|
|
69
|
+
if (tid && SENSOR_TYPES_20[tid]) {
|
|
70
|
+
$('#sensor20-type-hint').text(SENSOR_TYPES_20[tid].name + ' · up to 32ch · energy counters').show();
|
|
71
|
+
$('#node-input-typeId').val('0x' + tid.toString(16).padStart(2,'0').toUpperCase());
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
// Deferred load — TypedInput sets value async after oneditprepare
|
|
75
|
+
|
|
76
|
+
$('#node-input-bridge').trigger('change');
|
|
77
|
+
|
|
78
|
+
setTimeout(function() { $('#node-input-bridge').trigger('change'); }, 150);
|
|
79
|
+
},
|
|
80
|
+
|
|
81
|
+
oneditsave: function() {
|
|
82
|
+
this.address = parseInt($('#node-input-address').val()) || 0;
|
|
83
|
+
const tid = $('#node-input-typeId').val();
|
|
84
|
+
this.typeId = tid || '';
|
|
85
|
+
if (tid) {
|
|
86
|
+
const t = SENSOR_TYPES_20[parseInt(tid, 16)];
|
|
87
|
+
if (t) this.moduleName = t.name;
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
})();
|
|
93
|
+
</script>
|
|
94
|
+
|
|
95
|
+
<script type="text/html" data-template-name="velbus-sensor-20">
|
|
96
|
+
<div class="form-row">
|
|
97
|
+
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
|
98
|
+
<input type="text" id="node-input-name" placeholder="Optional label">
|
|
99
|
+
</div>
|
|
100
|
+
<div class="form-row">
|
|
101
|
+
<label for="node-input-bridge"><i class="fa fa-plug"></i> Bridge</label>
|
|
102
|
+
<input type="text" id="node-input-bridge">
|
|
103
|
+
</div>
|
|
104
|
+
<div class="form-row">
|
|
105
|
+
<label for="node-input-address"><i class="fa fa-map-marker"></i> Address</label>
|
|
106
|
+
<select id="node-input-address" style="width:70%">
|
|
107
|
+
<option value="">— select bridge first —</option>
|
|
108
|
+
</select>
|
|
109
|
+
</div>
|
|
110
|
+
<div class="form-row">
|
|
111
|
+
<label></label>
|
|
112
|
+
<span id="sensor20-type-hint" style="color:#888; font-size:0.9em; display:none;"></span>
|
|
113
|
+
</div>
|
|
114
|
+
<div class="form-row" style="display:none">
|
|
115
|
+
<input type="text" id="node-input-typeId">
|
|
116
|
+
<input type="text" id="node-input-moduleName">
|
|
117
|
+
</div>
|
|
118
|
+
</script>
|
|
119
|
+
|
|
120
|
+
<script type="text/html" data-help-name="velbus-sensor-20">
|
|
121
|
+
<p style="background:#fff3cd; border-left:4px solid #ffc107; padding:8px 12px; margin-bottom:12px; font-size:0.85em;">
|
|
122
|
+
<strong>⚠ Testing status:</strong> This node was generated with Claude.ai and is in
|
|
123
|
+
need of extensive field testing before being deployed into a commercial project.
|
|
124
|
+
It is presented "as is" — any use beyond testing is entirely at your own risk and liability.
|
|
125
|
+
Constructive feedback is welcomed, accompanied by as many examples and debug captures as possible.
|
|
126
|
+
<a href="https://github.com/MDAR/node-red-contrib-velbus-2026/issues" target="_blank">File an issue on GitHub</a>.
|
|
127
|
+
</p>
|
|
128
|
+
<p>V2 universal input node for VMB8IN-20 (0x4E).</p>
|
|
129
|
+
<p>Supports up to 32 digital input channels (8 primary + 24 via subaddresses),
|
|
130
|
+
8 energy counters, firmware check on startup, and name auto-retrieval.</p>
|
|
131
|
+
<p>For original series use <strong>velbus-sensor</strong>.</p>
|
|
132
|
+
|
|
133
|
+
<h3>Output 1 — Channel events / status</h3>
|
|
134
|
+
<p>Channel event (0x00) — includes sourceAddress to identify primary vs subaddress:</p>
|
|
135
|
+
<pre>{ "type": "channel", "on": true, "pressed": [1,3], "released": [],
|
|
136
|
+
"longPressed": [], "sourceAddress": "0x0A" }</pre>
|
|
137
|
+
<p>Primary module status (0xED — 8 bytes):</p>
|
|
138
|
+
<pre>{ "type": "status", "on": true, "channels": [1,2],
|
|
139
|
+
"enabled": [1,2,3,4,5,6,7,8], "normal": [1,2,3,4,5,6,7,8],
|
|
140
|
+
"locked": [], "progDisabled": [], "program": "group1",
|
|
141
|
+
"autoSendInterval": 30 }</pre>
|
|
142
|
+
<p>Subaddress status (0xED — 5 bytes):</p>
|
|
143
|
+
<pre>{ "type": "sub_status", "channels": [1,3], "enabled": [...], "locked": [], "progDisabled": [] }</pre>
|
|
144
|
+
|
|
145
|
+
<h3>Output 2 — Energy counter (0xA4)</h3>
|
|
146
|
+
<pre>{ "type": "energy", "channel": 1, "powerW": 1250, "energyWh": 45678 }</pre>
|
|
147
|
+
|
|
148
|
+
<h3>Input commands</h3>
|
|
149
|
+
<dl>
|
|
150
|
+
<dt><code>{ "cmd": "get_status" }</code></dt><dd>Request module status.</dd>
|
|
151
|
+
<dt><code>{ "cmd": "get_counter", "channels": 255, "interval": 30 }</code></dt>
|
|
152
|
+
<dd>Request energy counters. channels=bitmask of ch1-8 (255=all). interval=0 no change.</dd>
|
|
153
|
+
<dt><code>{ "cmd": "load_counter", "channel": 1, "value": 0 }</code></dt><dd>Load counter with preset value.</dd>
|
|
154
|
+
<dt><code>{ "cmd": "lock", "channel": 1, "duration": 3600 }</code></dt><dd>Lock channel by number (1-32, 255=all).</dd>
|
|
155
|
+
<dt><code>{ "cmd": "unlock", "channel": 1 }</code></dt><dd>Unlock channel.</dd>
|
|
156
|
+
<dt><code>{ "cmd": "get_name", "channel": 1 }</code></dt><dd>Request channel name (1-32, 255=all).</dd>
|
|
157
|
+
</dl>
|
|
158
|
+
</script>
|