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,279 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { pkt, rtrPkt, parsePkt } = require('../../lib/velbus-utils');
|
|
4
|
+
|
|
5
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
6
|
+
// Helpers
|
|
7
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
8
|
+
|
|
9
|
+
function activeFromBitmask(byte, count) {
|
|
10
|
+
const active = [];
|
|
11
|
+
for (let i = 0; i < count; i++) {
|
|
12
|
+
if (byte & (1 << i)) active.push('alarm' + (i + 1));
|
|
13
|
+
}
|
|
14
|
+
return active;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function decodeBitmask(byte, count) {
|
|
18
|
+
const result = {};
|
|
19
|
+
for (let i = 0; i < count; i++) {
|
|
20
|
+
result['alarm' + (i + 1)] = !!(byte & (1 << i));
|
|
21
|
+
}
|
|
22
|
+
return result;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function tempFrom16(hi, lo) {
|
|
26
|
+
const raw = (hi << 8) | lo;
|
|
27
|
+
return (raw > 32767 ? raw - 65536 : raw) / 512; // °C × 512, 0.0625° res
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
31
|
+
// Node
|
|
32
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
module.exports = function(RED) {
|
|
35
|
+
|
|
36
|
+
function VelbusMeteoNode(config) {
|
|
37
|
+
RED.nodes.createNode(this, config);
|
|
38
|
+
const node = this;
|
|
39
|
+
|
|
40
|
+
node.bridge = RED.nodes.getNode(config.bridge);
|
|
41
|
+
node.address = typeof config.address === 'number'
|
|
42
|
+
? config.address // legacy saves stored decimal numbers
|
|
43
|
+
: (parseInt(config.address, 16) || 0); // editor stores hex strings
|
|
44
|
+
|
|
45
|
+
if (!node.bridge) {
|
|
46
|
+
node.status({ fill: 'red', shape: 'ring', text: 'no bridge' });
|
|
47
|
+
node.error('velbus-meteo: no bridge configured');
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if (!node.address || node.address < 1 || node.address > 254) {
|
|
51
|
+
node.status({ fill: 'red', shape: 'ring', text: 'invalid address' });
|
|
52
|
+
node.error('velbus-meteo: invalid address ' + node.address);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const _addrHex = '0x' + node.address.toString(16).padStart(2, '0').toUpperCase();
|
|
57
|
+
|
|
58
|
+
function setStatus(text, fill, shape) {
|
|
59
|
+
node.status({ fill: fill || 'green', shape: shape || 'dot',
|
|
60
|
+
text: _addrHex + ' ' + text });
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Alarm channel names — keyed by bitmask position (0-7)
|
|
64
|
+
const _alarmNames = {};
|
|
65
|
+
const _nameParts = {}; // keyed by alarmBit then part (0/1/2)
|
|
66
|
+
|
|
67
|
+
function assembleNames() {
|
|
68
|
+
for (let bit = 0; bit < 8; bit++) {
|
|
69
|
+
const mask = 1 << bit;
|
|
70
|
+
const parts = _nameParts[mask];
|
|
71
|
+
if (!parts) continue;
|
|
72
|
+
const bytes = [
|
|
73
|
+
...(parts[0] || []),
|
|
74
|
+
...(parts[1] || []),
|
|
75
|
+
...(parts[2] || []),
|
|
76
|
+
].filter(b => b !== 0 && b !== 0xFF);
|
|
77
|
+
const name = String.fromCharCode(...bytes).trim();
|
|
78
|
+
if (name) _alarmNames[bit] = name;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ── Packet handler ────────────────────────────────────────────────────
|
|
83
|
+
|
|
84
|
+
function onPacket(raw) {
|
|
85
|
+
const p = parsePkt(raw);
|
|
86
|
+
if (!p || p.rtr) return;
|
|
87
|
+
if (p.addr !== node.address) return;
|
|
88
|
+
|
|
89
|
+
const { cmd, body } = p;
|
|
90
|
+
|
|
91
|
+
switch (cmd) {
|
|
92
|
+
|
|
93
|
+
// ── 0x00 Alarm output events ─────────────────────────────────────
|
|
94
|
+
case 0x00: {
|
|
95
|
+
if (body.length < 4) return;
|
|
96
|
+
// body[0]=cmd, body[1]=pressed, body[2]=released, body[3]=longPressed
|
|
97
|
+
const pressed = activeFromBitmask(body[1], 8);
|
|
98
|
+
const released = activeFromBitmask(body[2], 8);
|
|
99
|
+
const longPressed = activeFromBitmask(body[3], 8);
|
|
100
|
+
const on = pressed.length > 0 || longPressed.length > 0;
|
|
101
|
+
|
|
102
|
+
const payload = { type: 'alarm', on, pressed, released, longPressed };
|
|
103
|
+
if (pressed.length) setStatus('alarm: ' + pressed.join(','));
|
|
104
|
+
node.send([{ payload }, null]);
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ── 0xED Module status ───────────────────────────────────────────
|
|
109
|
+
case 0xED: {
|
|
110
|
+
if (body.length < 7) return;
|
|
111
|
+
// body[0]=0xED, body[1]=alarm bitmask, body[2]=locked, body[3]=prog-disabled
|
|
112
|
+
// body[4]=clock alarm & program, body[5]=auto-send interval, body[6]=test mode
|
|
113
|
+
const alarms = decodeBitmask(body[1], 8);
|
|
114
|
+
const locked = decodeBitmask(body[2], 8);
|
|
115
|
+
const progDisabled = decodeBitmask(body[3], 8);
|
|
116
|
+
|
|
117
|
+
const progByte = body[4];
|
|
118
|
+
const program = ['none', 'group1', 'group2', 'group3'][progByte & 0x03];
|
|
119
|
+
const alarm1Active = !!(progByte & 0x04);
|
|
120
|
+
const alarm2Active = !!(progByte & 0x10);
|
|
121
|
+
const sunriseEnabled = !!(progByte & 0x40);
|
|
122
|
+
const sunsetEnabled = !!(progByte & 0x80);
|
|
123
|
+
|
|
124
|
+
const autoSendInterval = body[5];
|
|
125
|
+
const testMode = !!(body[6] & 0x80);
|
|
126
|
+
|
|
127
|
+
const on = Object.values(alarms).some(v => v);
|
|
128
|
+
const payload = {
|
|
129
|
+
type: 'status',
|
|
130
|
+
on,
|
|
131
|
+
alarms,
|
|
132
|
+
locked,
|
|
133
|
+
progDisabled,
|
|
134
|
+
program,
|
|
135
|
+
clockAlarms: { alarm1Active, alarm2Active },
|
|
136
|
+
sunrise: sunriseEnabled,
|
|
137
|
+
sunset: sunsetEnabled,
|
|
138
|
+
autoSendInterval,
|
|
139
|
+
testMode,
|
|
140
|
+
};
|
|
141
|
+
node.send([{ payload }, null]);
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ── 0xE6 Temperature ─────────────────────────────────────────────
|
|
146
|
+
case 0xE6: {
|
|
147
|
+
if (body.length < 3) return;
|
|
148
|
+
// body[0]=0xE6, body[1-2]=current, body[3-4]=min, body[5-6]=max
|
|
149
|
+
const current = tempFrom16(body[1], body[2]);
|
|
150
|
+
const min = body.length >= 5 ? tempFrom16(body[3], body[4]) : null;
|
|
151
|
+
const max = body.length >= 7 ? tempFrom16(body[5], body[6]) : null;
|
|
152
|
+
|
|
153
|
+
const payload = { type: 'temperature', current };
|
|
154
|
+
if (min !== null) payload.min = min;
|
|
155
|
+
if (max !== null) payload.max = max;
|
|
156
|
+
node.send([null, { payload }]);
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ── 0xA9 Rain / light / wind raw values ─────────────────────────
|
|
161
|
+
case 0xA9: {
|
|
162
|
+
if (body.length < 7) return;
|
|
163
|
+
// body[0]=0xA9, body[1-2]=rain, body[3-4]=light, body[5-6]=wind
|
|
164
|
+
const rainRaw = (body[1] << 8) | body[2];
|
|
165
|
+
const lightRaw = (body[3] << 8) | body[4];
|
|
166
|
+
const windRaw = (body[5] << 8) | body[6];
|
|
167
|
+
|
|
168
|
+
const payload = {
|
|
169
|
+
type: 'meteo',
|
|
170
|
+
rain: rainRaw / 10, // mm/h
|
|
171
|
+
light: lightRaw, // lux
|
|
172
|
+
wind: windRaw / 10, // km/h
|
|
173
|
+
raw: { rain: rainRaw, light: lightRaw, wind: windRaw },
|
|
174
|
+
};
|
|
175
|
+
setStatus('wind:' + payload.wind.toFixed(1) + ' rain:' + payload.rain.toFixed(1));
|
|
176
|
+
node.send([null, { payload }]);
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ── 0xAC Sensor text string ──────────────────────────────────────
|
|
181
|
+
case 0xAC: {
|
|
182
|
+
if (body.length < 4) return;
|
|
183
|
+
// body[0]=0xAC, body[1]=sensor bitmask, body[2]=start position, body[3..]=chars
|
|
184
|
+
const sensorByte = body[1];
|
|
185
|
+
const sensor = sensorByte === 0x02 ? 'rain'
|
|
186
|
+
: sensorByte === 0x04 ? 'light'
|
|
187
|
+
: sensorByte === 0x08 ? 'wind'
|
|
188
|
+
: 'unknown';
|
|
189
|
+
const startPos = body[2];
|
|
190
|
+
let text = '';
|
|
191
|
+
for (let i = 3; i < body.length; i++) {
|
|
192
|
+
if (body[i] === 0) break;
|
|
193
|
+
text += String.fromCharCode(body[i]);
|
|
194
|
+
}
|
|
195
|
+
const payload = { type: 'meteo_text', sensor, startPos, text };
|
|
196
|
+
node.send([null, { payload }]);
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// ── 0xF0/F1/F2 Alarm channel name parts ─────────────────────────
|
|
201
|
+
case 0xF0:
|
|
202
|
+
case 0xF1:
|
|
203
|
+
case 0xF2: {
|
|
204
|
+
// body[0]=cmd, body[1]=channel bitmask, body[2..]=chars
|
|
205
|
+
const mask = body[1];
|
|
206
|
+
const part = cmd - 0xF0;
|
|
207
|
+
if (!_nameParts[mask]) _nameParts[mask] = {};
|
|
208
|
+
_nameParts[mask][part] = Array.from(body).slice(2);
|
|
209
|
+
if (part === 2) assembleNames();
|
|
210
|
+
break;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
default:
|
|
214
|
+
break;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
node.bridge.register(node.address, onPacket);
|
|
219
|
+
|
|
220
|
+
// ── Input commands ────────────────────────────────────────────────────
|
|
221
|
+
|
|
222
|
+
node.on('input', function(msg) {
|
|
223
|
+
const cmd = msg.payload && msg.payload.cmd;
|
|
224
|
+
if (!cmd) return;
|
|
225
|
+
|
|
226
|
+
if (cmd === 'get_status') {
|
|
227
|
+
node.bridge.send(pkt(0xFB, node.address, [0xFA, 0x00]));
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Request temperature — DB2=0x00 means temp, interval in seconds
|
|
232
|
+
if (cmd === 'get_temp') {
|
|
233
|
+
const interval = parseInt(msg.payload.interval) || 0;
|
|
234
|
+
node.bridge.send(pkt(0xF8, node.address, [0xE5, 0x00, interval]));
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Request sensor (rain/light/wind) — DB2=sensor bitmask, DB3=interval
|
|
239
|
+
if (cmd === 'get_meteo') {
|
|
240
|
+
const sensorMap = { rain: 0x02, light: 0x04, wind: 0x08 };
|
|
241
|
+
const sensor = msg.payload.sensor || 'all';
|
|
242
|
+
let mask = 0;
|
|
243
|
+
if (sensor === 'all') {
|
|
244
|
+
mask = 0x02 | 0x04 | 0x08;
|
|
245
|
+
} else if (typeof sensor === 'string' && sensorMap[sensor]) {
|
|
246
|
+
mask = sensorMap[sensor];
|
|
247
|
+
} else {
|
|
248
|
+
mask = 0x0E; // all three
|
|
249
|
+
}
|
|
250
|
+
const interval = parseInt(msg.payload.interval) || 0;
|
|
251
|
+
node.bridge.send(pkt(0xF8, node.address, [0xE5, mask, interval]));
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Request alarm channel name — DB2=channel bitmask
|
|
256
|
+
if (cmd === 'get_alarm_name') {
|
|
257
|
+
const ch = parseInt(msg.payload.channel) || 0;
|
|
258
|
+
const mask = ch >= 1 && ch <= 8 ? (1 << (ch - 1)) : 0xFF;
|
|
259
|
+
node.bridge.send(pkt(0xF8, node.address, [0xEF, mask]));
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (cmd === 'test_on') { node.bridge.send(pkt(0xF8, node.address, [0xB5, 0x01])); return; }
|
|
264
|
+
if (cmd === 'test_off') { node.bridge.send(pkt(0xF8, node.address, [0xB5, 0x00])); return; }
|
|
265
|
+
|
|
266
|
+
node.warn('velbus-meteo: unknown cmd: ' + cmd);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
// ── Cleanup ───────────────────────────────────────────────────────────
|
|
270
|
+
|
|
271
|
+
node.on('close', function() {
|
|
272
|
+
node.bridge.deregister(node.address, onPacket);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
setStatus('ready', 'grey', 'dot');
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
RED.nodes.registerType('velbus-meteo', VelbusMeteoNode);
|
|
279
|
+
};
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
<script type="text/javascript">
|
|
2
|
+
(function() {
|
|
3
|
+
|
|
4
|
+
const PIR_TYPES = {
|
|
5
|
+
0x2A: { name: 'VMBPIRM', hasTempSensor: false, series: 'original' },
|
|
6
|
+
0x2B: { name: 'VMBPIRC', hasTempSensor: false, series: 'original' },
|
|
7
|
+
0x2C: { name: 'VMBPIRO', hasTempSensor: true, series: 'original' },
|
|
8
|
+
0x23: { name: 'VMBPIRO-10', hasTempSensor: true, series: '-10' },
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
RED.nodes.registerType('velbus-pir', {
|
|
12
|
+
category: 'Velbus (inputs)',
|
|
13
|
+
color: '#3A8C8C',
|
|
14
|
+
defaults: {
|
|
15
|
+
name: { value: '' },
|
|
16
|
+
bridge: { value: '', type: 'velbus-bridge', required: true },
|
|
17
|
+
address: { value: '', required: true, validate: function(v) { return /^(0x)?[0-9a-fA-F]{1,2}$/.test(v); } },
|
|
18
|
+
moduleName: { value: '' },
|
|
19
|
+
typeId: { value: '' },
|
|
20
|
+
},
|
|
21
|
+
inputs: 1,
|
|
22
|
+
outputs: 2,
|
|
23
|
+
outputLabels: ['channel / status / light', 'temperature'],
|
|
24
|
+
icon: 'font-awesome/fa-eye',
|
|
25
|
+
paletteLabel: 'pir',
|
|
26
|
+
|
|
27
|
+
label: function() {
|
|
28
|
+
const t = this.typeId ? PIR_TYPES[parseInt(this.typeId, 16)] : null;
|
|
29
|
+
const n = this.name || this.moduleName || (t ? t.name : 'PIR');
|
|
30
|
+
return n + ' 0x' + (parseInt(this.address) || 0).toString(16).padStart(2,'0').toUpperCase();
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
oneditprepare: function() {
|
|
34
|
+
const savedAddr = this.address;
|
|
35
|
+
const savedTypeId = this.typeId ? parseInt(this.typeId, 16) : null;
|
|
36
|
+
|
|
37
|
+
$('#node-input-typeId').prop('readonly', true).css('background', '#f0f0f0');
|
|
38
|
+
|
|
39
|
+
function populateDropdown(bridgeId) {
|
|
40
|
+
if (!bridgeId) return;
|
|
41
|
+
$.getJSON('velbus/scan-results?bridge=' + bridgeId, function(results) {
|
|
42
|
+
const modules = (results && results.modules) ? results.modules : [];
|
|
43
|
+
if (!modules.length) return;
|
|
44
|
+
|
|
45
|
+
const $addr = $('#node-input-address');
|
|
46
|
+
$addr.empty().append($('<option>').val('').text('— select address —'));
|
|
47
|
+
|
|
48
|
+
const pirTypeIds = Object.keys(PIR_TYPES).map(k => parseInt(k));
|
|
49
|
+
const pirModules = modules.filter(m => pirTypeIds.includes(parseInt(m.typeId, 16)));
|
|
50
|
+
|
|
51
|
+
if (!pirModules.length) {
|
|
52
|
+
$addr.append($('<option>').val('').text('No PIR modules found on bus'));
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
pirModules.forEach(function(m) {
|
|
57
|
+
const tid = parseInt(m.typeId, 16);
|
|
58
|
+
const label = m.address + ' — ' + m.module;
|
|
59
|
+
const $opt = $('<option>').val(m.addressDec).text(label).data('typeid', tid);
|
|
60
|
+
if (m.addressDec === parseInt(savedAddr)) $opt.attr('selected', true);
|
|
61
|
+
$addr.append($opt);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
if (savedAddr) updateHint(parseInt(savedAddr));
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function updateHint(addrDec) {
|
|
69
|
+
const $opt = $('#node-input-address option[value="' + addrDec + '"]');
|
|
70
|
+
const tid = $opt.data('typeid');
|
|
71
|
+
if (tid && PIR_TYPES[tid]) {
|
|
72
|
+
const t = PIR_TYPES[tid];
|
|
73
|
+
$('#pir-type-hint').text(
|
|
74
|
+
t.name + ' · ' + t.series +
|
|
75
|
+
(t.hasTempSensor ? ' · temperature sensor' : '')
|
|
76
|
+
).show();
|
|
77
|
+
$('#node-input-typeId').val('0x' + tid.toString(16).padStart(2,'0').toUpperCase());
|
|
78
|
+
} else {
|
|
79
|
+
$('#pir-type-hint').hide();
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
$('#node-input-bridge').on('change', function() { populateDropdown($(this).val()); });
|
|
84
|
+
$('#node-input-address').on('change', function() { updateHint(parseInt($(this).val())); });
|
|
85
|
+
|
|
86
|
+
// Deferred load — TypedInput sets value async after oneditprepare
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
$('#node-input-bridge').trigger('change');
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
setTimeout(function() { $('#node-input-bridge').trigger('change'); }, 150);
|
|
93
|
+
},
|
|
94
|
+
|
|
95
|
+
oneditsave: function() {
|
|
96
|
+
this.address = parseInt($('#node-input-address').val()) || 0;
|
|
97
|
+
const tid = $('#node-input-typeId').val();
|
|
98
|
+
this.typeId = tid || '';
|
|
99
|
+
if (tid) {
|
|
100
|
+
const t = PIR_TYPES[parseInt(tid, 16)];
|
|
101
|
+
if (t) this.moduleName = t.name;
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
})();
|
|
107
|
+
</script>
|
|
108
|
+
|
|
109
|
+
<script type="text/html" data-template-name="velbus-pir">
|
|
110
|
+
<div class="form-row">
|
|
111
|
+
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
|
112
|
+
<input type="text" id="node-input-name" placeholder="Optional label">
|
|
113
|
+
</div>
|
|
114
|
+
<div class="form-row">
|
|
115
|
+
<label for="node-input-bridge"><i class="fa fa-plug"></i> Bridge</label>
|
|
116
|
+
<input type="text" id="node-input-bridge">
|
|
117
|
+
</div>
|
|
118
|
+
<div class="form-row">
|
|
119
|
+
<label for="node-input-address"><i class="fa fa-map-marker"></i> Address</label>
|
|
120
|
+
<select id="node-input-address" style="width:70%">
|
|
121
|
+
<option value="">— select bridge first —</option>
|
|
122
|
+
</select>
|
|
123
|
+
</div>
|
|
124
|
+
<div class="form-row">
|
|
125
|
+
<label></label>
|
|
126
|
+
<span id="pir-type-hint" style="color:#888; font-size:0.9em; display:none;"></span>
|
|
127
|
+
</div>
|
|
128
|
+
<div class="form-row" style="display:none">
|
|
129
|
+
<input type="text" id="node-input-typeId">
|
|
130
|
+
<input type="text" id="node-input-moduleName">
|
|
131
|
+
</div>
|
|
132
|
+
</script>
|
|
133
|
+
|
|
134
|
+
<script type="text/html" data-help-name="velbus-pir">
|
|
135
|
+
<p style="background:#fff3cd; border-left:4px solid #ffc107; padding:8px 12px; margin-bottom:12px; font-size:0.85em;">
|
|
136
|
+
<strong>⚠ Testing status:</strong> This node was generated with Claude.ai and is in
|
|
137
|
+
need of extensive field testing before being deployed into a commercial project.
|
|
138
|
+
It is presented "as is" — any use beyond testing is entirely at your own risk and liability.
|
|
139
|
+
Constructive feedback is welcomed, accompanied by as many examples and debug captures as possible.
|
|
140
|
+
<a href="https://github.com/MDAR/node-red-contrib-velbus-2026/issues" target="_blank">File an issue on GitHub</a>.
|
|
141
|
+
</p>
|
|
142
|
+
<p>
|
|
143
|
+
PIR motion sensor node for Velbus original and -10 series modules.
|
|
144
|
+
For V2 (-20) series use <strong>velbus-pir-20</strong>.
|
|
145
|
+
</p>
|
|
146
|
+
|
|
147
|
+
<h3>Supported modules</h3>
|
|
148
|
+
<p>VMBPIRM (0x2A), VMBPIRC (0x2B), VMBPIRO (0x2C), VMBPIRO-10 (0x23).</p>
|
|
149
|
+
<p>VMBPIRO and VMBPIRO-10 include a temperature sensor — output 2 is active on these types.</p>
|
|
150
|
+
|
|
151
|
+
<h3>Output 1 — Channel / status / light</h3>
|
|
152
|
+
<p>Channel event (0x00):</p>
|
|
153
|
+
<pre>{
|
|
154
|
+
"type": "channel",
|
|
155
|
+
"on": true,
|
|
156
|
+
"pressed": ["motion1", "dark"],
|
|
157
|
+
"released": [],
|
|
158
|
+
"longPressed": []
|
|
159
|
+
}</pre>
|
|
160
|
+
<p>Module status (0xED):</p>
|
|
161
|
+
<pre>{
|
|
162
|
+
"type": "status",
|
|
163
|
+
"on": true,
|
|
164
|
+
"channels": { "dark": false, "light": true, "motion1": true, ... },
|
|
165
|
+
"lightRaw": 1024,
|
|
166
|
+
"testMode": false,
|
|
167
|
+
"locked": { "dark": false, ... },
|
|
168
|
+
"program": "summer",
|
|
169
|
+
"alarms": { "alarm1Active": false, "alarm2Active": false }
|
|
170
|
+
}</pre>
|
|
171
|
+
<p>Light value (0xA9):</p>
|
|
172
|
+
<pre>{ "type": "light", "on": true, "raw": 1024 }</pre>
|
|
173
|
+
|
|
174
|
+
<h3>Output 2 — Temperature (VMBPIRO / VMBPIRO-10 only)</h3>
|
|
175
|
+
<pre>{ "type": "temperature", "current": 21.5, "min": 18.0, "max": 25.0 }</pre>
|
|
176
|
+
<p>Settings (0xE8):</p>
|
|
177
|
+
<pre>{ "type": "temp_settings", "calOffset": 0, "calGain": 128, "lowAlarm": 5.0, "highAlarm": 35.0, "zone": 0 }</pre>
|
|
178
|
+
|
|
179
|
+
<h3>Input commands</h3>
|
|
180
|
+
<dl>
|
|
181
|
+
<dt><code>{ "cmd": "get_status" }</code></dt><dd>Request module status (0xFA).</dd>
|
|
182
|
+
<dt><code>{ "cmd": "get_light", "interval": 30 }</code></dt><dd>Request light value. interval=0 means no change to auto-send setting.</dd>
|
|
183
|
+
<dt><code>{ "cmd": "get_temp" }</code></dt><dd>Request temperature (VMBPIRO/VMBPIRO-10 only).</dd>
|
|
184
|
+
<dt><code>{ "cmd": "get_temp_settings" }</code></dt><dd>Request temperature calibration and alarm settings.</dd>
|
|
185
|
+
<dt><code>{ "cmd": "test_on" }</code> / <code>{ "cmd": "test_off" }</code></dt><dd>Enable/disable test mode (30 minute timeout).</dd>
|
|
186
|
+
</dl>
|
|
187
|
+
</script>
|