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.
Files changed (56) hide show
  1. package/CHANGELOG_FORUM.md +784 -0
  2. package/LICENSE +21 -0
  3. package/README.md +140 -0
  4. package/examples/velbus-basic-relay-dimmer.json +328 -0
  5. package/index.js +3 -0
  6. package/lib/blind-types-20.js +26 -0
  7. package/lib/blind-types-s.js +46 -0
  8. package/lib/blind-types.js +38 -0
  9. package/lib/dimmer-types-20.js +120 -0
  10. package/lib/dimmer-types.js +34 -0
  11. package/lib/glass-panel-types.js +296 -0
  12. package/lib/pir-types-20.js +51 -0
  13. package/lib/pir-types.js +61 -0
  14. package/lib/relay-types-20.js +43 -0
  15. package/lib/relay-types.js +39 -0
  16. package/lib/sensor-types-20.js +29 -0
  17. package/lib/sensor-types.js +49 -0
  18. package/lib/velbus-utils.js +80 -0
  19. package/nodes/velbus-blind/velbus-blind.html +165 -0
  20. package/nodes/velbus-blind/velbus-blind.js +251 -0
  21. package/nodes/velbus-blind-20/velbus-blind-20.html +179 -0
  22. package/nodes/velbus-blind-20/velbus-blind-20.js +300 -0
  23. package/nodes/velbus-blind-s/velbus-blind-s.html +181 -0
  24. package/nodes/velbus-blind-s/velbus-blind-s.js +282 -0
  25. package/nodes/velbus-bridge/velbus-bridge.html +72 -0
  26. package/nodes/velbus-bridge/velbus-bridge.js +304 -0
  27. package/nodes/velbus-button/velbus-button.html +172 -0
  28. package/nodes/velbus-button/velbus-button.js +117 -0
  29. package/nodes/velbus-clock/velbus-clock.html +198 -0
  30. package/nodes/velbus-clock/velbus-clock.js +273 -0
  31. package/nodes/velbus-dimmer/velbus-dimmer.html +174 -0
  32. package/nodes/velbus-dimmer/velbus-dimmer.js +457 -0
  33. package/nodes/velbus-dimmer-20/velbus-dimmer-20.html +271 -0
  34. package/nodes/velbus-dimmer-20/velbus-dimmer-20.js +602 -0
  35. package/nodes/velbus-glass-panel/velbus-glass-panel.html +337 -0
  36. package/nodes/velbus-glass-panel/velbus-glass-panel.js +492 -0
  37. package/nodes/velbus-meteo/velbus-meteo.html +120 -0
  38. package/nodes/velbus-meteo/velbus-meteo.js +279 -0
  39. package/nodes/velbus-pir/velbus-pir.html +187 -0
  40. package/nodes/velbus-pir/velbus-pir.js +269 -0
  41. package/nodes/velbus-pir-20/velbus-pir-20.html +186 -0
  42. package/nodes/velbus-pir-20/velbus-pir-20.js +352 -0
  43. package/nodes/velbus-relay/velbus-relay.html +215 -0
  44. package/nodes/velbus-relay/velbus-relay.js +404 -0
  45. package/nodes/velbus-relay-20/velbus-relay-20.html +123 -0
  46. package/nodes/velbus-relay-20/velbus-relay-20.js +434 -0
  47. package/nodes/velbus-scan/velbus-scan.html +87 -0
  48. package/nodes/velbus-scan/velbus-scan.js +313 -0
  49. package/nodes/velbus-sensor/velbus-sensor.html +154 -0
  50. package/nodes/velbus-sensor/velbus-sensor.js +259 -0
  51. package/nodes/velbus-sensor-20/velbus-sensor-20.html +158 -0
  52. package/nodes/velbus-sensor-20/velbus-sensor-20.js +314 -0
  53. package/nodes/velbus-thermostat/velbus-thermostat.html +191 -0
  54. package/nodes/velbus-thermostat/velbus-thermostat.js +322 -0
  55. package/package.json +55 -0
  56. package/velbus-2026-repo.bundle +0 -0
@@ -0,0 +1,322 @@
1
+ 'use strict';
2
+
3
+ const { pkt, parsePkt } = require('../../lib/velbus-utils');
4
+
5
+ // ─────────────────────────────────────────────────────────────────────────────
6
+ // Thermostat mode constants
7
+ // ─────────────────────────────────────────────────────────────────────────────
8
+ const MODE_CMD = {
9
+ comfort: 0xDB,
10
+ day: 0xDC,
11
+ night: 0xDD,
12
+ safe: 0xDE,
13
+ };
14
+
15
+ const MODE_LABEL = {
16
+ 0: 'comfort',
17
+ 1: 'day',
18
+ 2: 'night',
19
+ 3: 'safe',
20
+ };
21
+
22
+ const THERM_STATUS_LABEL = {
23
+ 0: 'disabled',
24
+ 1: 'manual',
25
+ 2: 'timer',
26
+ 3: 'timer_override',
27
+ };
28
+
29
+ // ─────────────────────────────────────────────────────────────────────────────
30
+ // Node
31
+ // ─────────────────────────────────────────────────────────────────────────────
32
+ module.exports = function(RED) {
33
+
34
+ function VelbusThermostatNode(config) {
35
+ RED.nodes.createNode(this, config);
36
+ const node = this;
37
+
38
+ node.bridge = RED.nodes.getNode(config.bridge);
39
+ node.address = typeof config.address === 'number'
40
+ ? config.address // legacy saves stored decimal numbers
41
+ : (parseInt(config.address, 16) || 0); // editor stores hex strings
42
+ node.moduleName = config.moduleName || '';
43
+
44
+ if (!node.bridge) {
45
+ node.status({ fill: 'red', shape: 'ring', text: 'no bridge' });
46
+ node.error('velbus-thermostat: no bridge configured');
47
+ return;
48
+ }
49
+ if (!node.address || node.address < 1 || node.address > 254) {
50
+ node.status({ fill: 'red', shape: 'ring', text: 'invalid address' });
51
+ node.error('velbus-thermostat: invalid address ' + node.address);
52
+ return;
53
+ }
54
+
55
+ const _label = node.moduleName
56
+ ? node.moduleName + ' (0x' + node.address.toString(16).padStart(2, '0').toUpperCase() + ')'
57
+ : '0x' + node.address.toString(16).padStart(2, '0').toUpperCase();
58
+
59
+ // Name retrieval state
60
+ let _nameParts = {};
61
+ let _nameTimer = null;
62
+
63
+ function assembleName() {
64
+ if (_nameTimer) { clearTimeout(_nameTimer); _nameTimer = null; }
65
+ const bytes = [
66
+ ...(_nameParts[0] || []),
67
+ ...(_nameParts[1] || []),
68
+ ...(_nameParts[2] || []),
69
+ ].filter(b => b !== 0 && b !== 0xFF);
70
+ const name = String.fromCharCode(...bytes).trim();
71
+ if (name) node.moduleName = name;
72
+ }
73
+
74
+ function setStatus(text, fill, shape) {
75
+ node.status({ fill: fill || 'green', shape: shape || 'dot', text: _label + ' ' + text });
76
+ }
77
+
78
+ // ── Packet handler ───────────────────────────────────────────────────
79
+
80
+ function onPacket(raw) {
81
+ const p = parsePkt(raw);
82
+ if (!p || p.rtr) return;
83
+ // Note: no address re-check here. The bridge routes frames to this
84
+ // listener, including subaddress frames mapped to the primary address
85
+ // (e.g. GPO-20 thermostat status from subaddress 0x34).
86
+
87
+ const { cmd, body } = p;
88
+
89
+ switch (cmd) {
90
+
91
+ // ── 0xEA Thermostat status ────────────────────────────────────────
92
+ case 0xEA: {
93
+ if (body.length < 8) return;
94
+ // body[0]=0xEA cmd
95
+ // body[1]=DB2 operating mode
96
+ // body[2]=DB3 program step mode
97
+ // body[3]=DB4 output status (heater/cooler/pump/alarms)
98
+ // body[4]=DB5 current temp (signed byte, resolution 0.5°)
99
+ // body[5]=DB6 current target temp set (signed byte, resolution 0.5°)
100
+ // body[6]=DB7 sleep timer high byte
101
+ // body[7]=DB8 sleep timer low byte
102
+
103
+ const opMode = body[1];
104
+ const modeBits = (opMode >> 4) & 0x07;
105
+ const mode = modeBits === 4 ? 'comfort'
106
+ : modeBits === 2 ? 'day'
107
+ : modeBits === 1 ? 'night'
108
+ : 'safe';
109
+ const heaterMode = !(opMode & 0x80); // bit7: 0=heater, 1=cooler
110
+ // Bits 2-1: control state per GPO-20 protocol ed.3
111
+ const ctlBits = (opMode >> 1) & 0x03;
112
+ const controlState = ctlBits === 0 ? 'run'
113
+ : ctlBits === 1 ? 'manual'
114
+ : ctlBits === 2 ? 'sleep_timer'
115
+ : 'forced_safe_locked';
116
+ const thermostatOn = ctlBits !== 3; // legacy convenience flag
117
+
118
+ const outByte = body[3];
119
+ // Full DATABYTE4 output status per VMBGPO-20 protocol ed.3:
120
+ const heating = !!(outByte & 0x01);
121
+ const boostMode = !!(outByte & 0x02);
122
+ const pump = !!(outByte & 0x04);
123
+ const cooling = !!(outByte & 0x08);
124
+ const tempAlarm1 = !!(outByte & 0x10);
125
+ const tempAlarm2 = !!(outByte & 0x20);
126
+ const tempAlarm3 = !!(outByte & 0x40);
127
+ const tempAlarm4 = !!(outByte & 0x80);
128
+
129
+ const rawCurrent = body[4] > 127 ? body[4] - 256 : body[4];
130
+ const rawTarget = body[5] > 127 ? body[5] - 256 : body[5];
131
+
132
+ const payload = {
133
+ type: 'thermostat',
134
+ sourceAddr: '0x' + p.addr.toString(16).padStart(2, '0').toUpperCase(),
135
+ raw: [...raw].map(b => b.toString(16).padStart(2, '0').toUpperCase()).join(' '),
136
+ currentTemp: rawCurrent * 0.5,
137
+ targetTemp: rawTarget * 0.5,
138
+ mode,
139
+ heaterMode,
140
+ heating,
141
+ cooling,
142
+ boostMode,
143
+ pump,
144
+ tempAlarm1,
145
+ tempAlarm2,
146
+ tempAlarm3,
147
+ tempAlarm4,
148
+ controlState,
149
+ thermostatOn,
150
+ };
151
+
152
+ setStatus(
153
+ (thermostatOn ? 'on' : 'off') + ' · ' +
154
+ mode + ' · ' +
155
+ (rawCurrent * 0.5).toFixed(1) + '°C → ' + (rawTarget * 0.5).toFixed(1) + '°C'
156
+ );
157
+ node.send([{ payload }, null]);
158
+ break;
159
+ }
160
+
161
+ // ── 0xE6 Current temperature ─────────────────────────────────────
162
+ case 0xE6: {
163
+ if (body.length < 3) return;
164
+ // body[0]=0xE6 cmd, body[1-2]=DB2-3 current (16-bit, /16 = °C at 0.0625° res)
165
+ // body[3-4]=DB4-5 min, body[5-6]=DB6-7 max (optional)
166
+ // 16-bit signed, °C × 512 (0.0625° resolution, low 5 bits unused)
167
+ const t16 = (hi, lo) => { const r = (hi << 8) | lo; return (r > 32767 ? r - 65536 : r) / 512; };
168
+ const current = t16(body[1], body[2]);
169
+ const min = body.length >= 5 ? t16(body[3], body[4]) : null;
170
+ const max = body.length >= 7 ? t16(body[5], body[6]) : null;
171
+
172
+ const payload = { type: 'temperature', current };
173
+ if (min !== null) payload.min = min;
174
+ if (max !== null) payload.max = max;
175
+
176
+ node.send([null, { payload }]);
177
+ break;
178
+ }
179
+
180
+ // ── 0xE8 Temperature settings part 1 (heating presets) ──────────
181
+ case 0xE8: {
182
+ if (body.length < 9) return;
183
+ // body[0]=0xE8 cmd, body[1]=DB2 current set, body[2]=DB3 comfort heat,
184
+ // body[3]=DB4 day heat, body[4]=DB5 night heat, body[5]=DB6 safe heat,
185
+ // body[6]=DB7 boost diff, body[7]=DB8 hysteresis
186
+ // All signed bytes, resolution 0.5°
187
+ function signed05(b) { return (b > 127 ? b - 256 : b) * 0.5; }
188
+ const payload = {
189
+ type: 'temp_settings',
190
+ current: signed05(body[1]),
191
+ comfort: signed05(body[2]),
192
+ day: signed05(body[3]),
193
+ night: signed05(body[4]),
194
+ safe: signed05(body[5]),
195
+ };
196
+ node.send([{ payload }, null]);
197
+ break;
198
+ }
199
+
200
+ // ── 0xED Module status (thermostat programme field) ──────────────
201
+ case 0xED: {
202
+ if (body.length < 7) return;
203
+ // body[0]=0xED cmd, body[6]=DB7 alarm & program selection
204
+ const progByte = body[6];
205
+ const thermostatProgram = THERM_STATUS_LABEL[progByte & 0x03] || 'unknown';
206
+ const alarm1Active = !!(progByte & 0x04);
207
+ const alarm2Active = !!(progByte & 0x08);
208
+
209
+ const payload = {
210
+ type: 'programme',
211
+ thermostatProgram,
212
+ alarms: { alarm1Active, alarm2Active },
213
+ };
214
+ node.send([{ payload }, null]);
215
+ break;
216
+ }
217
+
218
+ // ── 0xF0/F1/F2 Name part response ─────────────────────────────────
219
+ case 0xF0:
220
+ case 0xF1:
221
+ case 0xF2: {
222
+ // body[0]=cmd, body[1]=DB2 channel number, body[2..]=DB3.. chars
223
+ const part = cmd - 0xF0;
224
+ _nameParts[part] = Array.from(body).slice(2);
225
+ if (part === 2) assembleName();
226
+ break;
227
+ }
228
+
229
+ default: {
230
+ // Unhandled command — emit raw on output 2 so subaddress channel
231
+ // events (0x00 push-button status from the sensor sub carrying
232
+ // heater/boost/pump/cooler/alarm states) are visible for analysis.
233
+ const payload = {
234
+ type: 'raw',
235
+ sourceAddr: '0x' + p.addr.toString(16).padStart(2, '0').toUpperCase(),
236
+ cmd: '0x' + cmd.toString(16).padStart(2, '0').toUpperCase(),
237
+ raw: [...raw].map(b => b.toString(16).padStart(2, '0').toUpperCase()).join(' '),
238
+ };
239
+ node.send([null, { payload }]);
240
+ break;
241
+ }
242
+ }
243
+ }
244
+
245
+ node.bridge.register(node.address, onPacket);
246
+
247
+ // Auto-retrieve module name on startup
248
+ setTimeout(function() {
249
+ node.bridge.send(pkt(0xF8, node.address, [0xEF]));
250
+ _nameTimer = setTimeout(assembleName, 2000);
251
+ }, 500);
252
+
253
+ // ── Input command handler ────────────────────────────────────────────
254
+
255
+ node.on('input', function(msg) {
256
+ const cmd = msg.payload && msg.payload.cmd;
257
+ if (!cmd) return;
258
+
259
+ // ── Mode switch ────────────────────────────────────────────────────
260
+ if (MODE_CMD[cmd] !== undefined) {
261
+ const sleep = (msg.payload.sleepTime !== undefined)
262
+ ? parseInt(msg.payload.sleepTime) : 0;
263
+ const sleepHi = (sleep >> 8) & 0xFF;
264
+ const sleepLo = sleep & 0xFF;
265
+ node.bridge.send(pkt(0xF8, node.address, [MODE_CMD[cmd], sleepHi, sleepLo]));
266
+ return;
267
+ }
268
+
269
+ // ── Set target temperature ─────────────────────────────────────────
270
+ if (cmd === 'set_temp') {
271
+ const pointer = (msg.payload.pointer !== undefined)
272
+ ? parseInt(msg.payload.pointer) : 0;
273
+ const tempRaw = Math.round((parseFloat(msg.payload.temp) || 0) * 100);
274
+ const tempHi = (tempRaw >> 8) & 0xFF;
275
+ const tempLo = tempRaw & 0xFF;
276
+ node.bridge.send(pkt(0xF8, node.address, [0xE4, pointer, tempHi, tempLo]));
277
+ return;
278
+ }
279
+
280
+ // ── Request thermostat status ──────────────────────────────────────
281
+ if (cmd === 'get_thermostat') {
282
+ node.bridge.send(pkt(0xF8, node.address, [0xE7]));
283
+ return;
284
+ }
285
+
286
+ // ── Request temperature preset settings ────────────────────────────
287
+ if (cmd === 'get_settings') {
288
+ node.bridge.send(pkt(0xF8, node.address, [0xE5]));
289
+ return;
290
+ }
291
+
292
+ // ── Request module status (thermostat programme field) ─────────────
293
+ if (cmd === 'get_status') {
294
+ node.bridge.send(pkt(0xFB, node.address, [0xFA]));
295
+ return;
296
+ }
297
+
298
+ // ── Heat / cool mode ───────────────────────────────────────────────
299
+ if (cmd === 'heat_mode') {
300
+ node.bridge.send(pkt(0xF8, node.address, [0xE0]));
301
+ return;
302
+ }
303
+ if (cmd === 'cool_mode') {
304
+ node.bridge.send(pkt(0xF8, node.address, [0xDF]));
305
+ return;
306
+ }
307
+
308
+ node.warn('velbus-thermostat: unknown cmd: ' + cmd);
309
+ });
310
+
311
+ // ── Cleanup ──────────────────────────────────────────────────────────
312
+
313
+ node.on('close', function() {
314
+ if (_nameTimer) { clearTimeout(_nameTimer); _nameTimer = null; }
315
+ node.bridge.deregister(node.address, onPacket);
316
+ });
317
+
318
+ setStatus('ready', 'grey', 'dot');
319
+ }
320
+
321
+ RED.nodes.registerType('velbus-thermostat', VelbusThermostatNode);
322
+ };
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "node-red-contrib-velbus-2026",
3
+ "version": "0.8.1",
4
+ "description": "Node-RED palette for Velbus building automation \u2014 relay, dimmer, glass panel, PIR, blind, sensor, meteo, clock nodes. Original and V2 (-20) series.",
5
+ "keywords": [
6
+ "node-red",
7
+ "velbus",
8
+ "building-automation",
9
+ "home-automation",
10
+ "smart-home",
11
+ "relay",
12
+ "dimmer"
13
+ ],
14
+ "node-red": {
15
+ "version": ">=2.0.0",
16
+ "nodes": {
17
+ "velbus-bridge": "nodes/velbus-bridge/velbus-bridge.js",
18
+ "velbus-scan": "nodes/velbus-scan/velbus-scan.js",
19
+ "velbus-relay": "nodes/velbus-relay/velbus-relay.js",
20
+ "velbus-relay-20": "nodes/velbus-relay-20/velbus-relay-20.js",
21
+ "velbus-dimmer": "nodes/velbus-dimmer/velbus-dimmer.js",
22
+ "velbus-dimmer-20": "nodes/velbus-dimmer-20/velbus-dimmer-20.js",
23
+ "velbus-glass-panel": "nodes/velbus-glass-panel/velbus-glass-panel.js",
24
+ "velbus-thermostat": "nodes/velbus-thermostat/velbus-thermostat.js",
25
+ "velbus-button": "nodes/velbus-button/velbus-button.js",
26
+ "velbus-pir": "nodes/velbus-pir/velbus-pir.js",
27
+ "velbus-pir-20": "nodes/velbus-pir-20/velbus-pir-20.js",
28
+ "velbus-meteo": "nodes/velbus-meteo/velbus-meteo.js",
29
+ "velbus-sensor": "nodes/velbus-sensor/velbus-sensor.js",
30
+ "velbus-sensor-20": "nodes/velbus-sensor-20/velbus-sensor-20.js",
31
+ "velbus-blind": "nodes/velbus-blind/velbus-blind.js",
32
+ "velbus-blind-s": "nodes/velbus-blind-s/velbus-blind-s.js",
33
+ "velbus-blind-20": "nodes/velbus-blind-20/velbus-blind-20.js",
34
+ "velbus-clock": "nodes/velbus-clock/velbus-clock.js"
35
+ },
36
+ "examples": "examples"
37
+ },
38
+ "main": "index.js",
39
+ "scripts": {
40
+ "test": "echo \"No tests yet\" && exit 0"
41
+ },
42
+ "author": "Stuart Hanlon <info@mdar.co.uk> (https://mdar.co.uk)",
43
+ "license": "MIT",
44
+ "engines": {
45
+ "node": ">=14.0.0"
46
+ },
47
+ "repository": {
48
+ "type": "git",
49
+ "url": "https://github.com/MDAR/node-red-contrib-velbus-2026.git"
50
+ },
51
+ "bugs": {
52
+ "url": "https://github.com/MDAR/node-red-contrib-velbus-2026/issues"
53
+ },
54
+ "homepage": "https://github.com/MDAR/node-red-contrib-velbus-2026#readme"
55
+ }
Binary file