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,314 @@
1
+ 'use strict';
2
+
3
+ const { pkt, rtrPkt, parsePkt } = require('../../lib/velbus-utils');
4
+ const { SENSOR_TYPES_20, SENSOR_TYPE_IDS_20 } = require('../../lib/sensor-types-20');
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
+ // ─────────────────────────────────────────────────────────────────────────────
19
+ // Node
20
+ // ─────────────────────────────────────────────────────────────────────────────
21
+
22
+ module.exports = function(RED) {
23
+
24
+ function VelbusSensor20Node(config) {
25
+ RED.nodes.createNode(this, config);
26
+ const node = this;
27
+
28
+ node.bridge = RED.nodes.getNode(config.bridge);
29
+ node.address = typeof config.address === 'number'
30
+ ? config.address // legacy saves stored decimal numbers
31
+ : (parseInt(config.address, 16) || 0); // editor stores hex strings
32
+ node.moduleName = config.moduleName || '';
33
+ node.typeId = config.typeId ? parseInt(config.typeId) : null;
34
+
35
+ if (!node.bridge) {
36
+ node.status({ fill: 'red', shape: 'ring', text: 'no bridge' });
37
+ node.error('velbus-sensor-20: no bridge configured');
38
+ return;
39
+ }
40
+ if (!node.address || node.address < 1 || node.address > 254) {
41
+ node.status({ fill: 'red', shape: 'ring', text: 'invalid address' });
42
+ node.error('velbus-sensor-20: invalid address ' + node.address);
43
+ return;
44
+ }
45
+
46
+ const typeDesc = node.typeId !== null ? (SENSOR_TYPES_20[node.typeId] || null) : null;
47
+
48
+ // Name retrieval state
49
+ let _nameParts = {};
50
+ let _nameTimer = null;
51
+
52
+ function assembleName() {
53
+ if (_nameTimer) { clearTimeout(_nameTimer); _nameTimer = null; }
54
+ const bytes = [
55
+ ...(_nameParts[0] || []),
56
+ ...(_nameParts[1] || []),
57
+ ...(_nameParts[2] || []),
58
+ ].filter(b => b !== 0 && b !== 0xFF);
59
+ const name = String.fromCharCode(...bytes).trim();
60
+ if (name) node.moduleName = name;
61
+ }
62
+
63
+ const _addrHex = '0x' + node.address.toString(16).padStart(2, '0').toUpperCase();
64
+
65
+ function setStatus(text, fill, shape) {
66
+ const label = node.moduleName
67
+ ? node.moduleName + ' (' + _addrHex + ')'
68
+ : _addrHex;
69
+ node.status({ fill: fill || 'green', shape: shape || 'dot', text: label + ' ' + text });
70
+ }
71
+
72
+ // ── Firmware check ────────────────────────────────────────────────────
73
+
74
+ function handleModuleType(body) {
75
+ if (body.length < 8) return;
76
+ const typeId = body[1];
77
+ const mapVer = body[4];
78
+ const build = body[5] * 100 + body[6];
79
+ const canFD = !!(body[7] & 0x20);
80
+
81
+ const desc = SENSOR_TYPES_20[typeId];
82
+ if (!desc) {
83
+ node.status({ fill: 'red', shape: 'ring',
84
+ text: 'unknown type 0x' + typeId.toString(16) });
85
+ node.error('velbus-sensor-20: unrecognised module type 0x' + typeId.toString(16));
86
+ return;
87
+ }
88
+
89
+ node.moduleName = node.moduleName || desc.name;
90
+ setStatus('build ' + build + (canFD ? ' CAN FD' : ''), 'grey', 'dot');
91
+
92
+ // Request name for all channels
93
+ setTimeout(() => {
94
+ node.bridge.send(pkt(0xF8, node.address, [0xEF, 0xFF]));
95
+ _nameTimer = setTimeout(assembleName, 2000);
96
+ }, 100);
97
+ }
98
+
99
+ // ── Packet handler ────────────────────────────────────────────────────
100
+
101
+ function onPacket(raw) {
102
+ const p = parsePkt(raw);
103
+ if (!p || p.rtr) return;
104
+ // Note: no address re-check here. The bridge routes frames to this
105
+ // listener, including subaddress frames mapped to the primary address
106
+ // (e.g. GPO-20 thermostat status from subaddress 0x34).
107
+
108
+ const { cmd, body } = p;
109
+
110
+ switch (cmd) {
111
+
112
+ // ── 0xFF Module type ─────────────────────────────────────────────
113
+ case 0xFF: {
114
+ handleModuleType(body);
115
+ break;
116
+ }
117
+
118
+ // ── 0x00 Channel events ──────────────────────────────────────────
119
+ case 0x00: {
120
+ if (body.length < 4) return;
121
+ // body[0]=cmd, body[1]=pressed, body[2]=released, body[3]=longPressed
122
+ // Note: may arrive from primary address OR a subaddress (bridge routes both)
123
+ const pressed = activeFromBitmask(body[1]);
124
+ const released = activeFromBitmask(body[2]);
125
+ const longPressed = activeFromBitmask(body[3]);
126
+ const on = pressed.length > 0 || longPressed.length > 0;
127
+
128
+ // Determine channel offset from source address
129
+ // Primary=ch1-8, sub1=ch9-16, sub2=ch17-24, sub3=ch25-32
130
+ // Bridge delivers all under primary address via subaddress routing
131
+ // We receive the raw packet, so p.addr tells us the source
132
+ // For now emit raw bitmask channels 1-8 — subaddress offsetting
133
+ // requires knowing which subaddress fired (handled via p.addr)
134
+ const srcAddr = p.addr;
135
+ const payload = {
136
+ type: 'channel',
137
+ on,
138
+ pressed,
139
+ released,
140
+ longPressed,
141
+ sourceAddress: '0x' + srcAddr.toString(16).padStart(2,'0').toUpperCase(),
142
+ };
143
+
144
+ if (pressed.length) setStatus('ch' + pressed.join(',') + ' on');
145
+ node.send([{ payload }, null]);
146
+ break;
147
+ }
148
+
149
+ // ── 0xED Module status (primary — 8 bytes) ───────────────────────
150
+ case 0xED: {
151
+ if (body.length < 8) {
152
+ // Subaddress status — 5 bytes
153
+ if (body.length < 5) return;
154
+ // body[0]=0xED, body[1]=alarm channel status, body[2]=enabled,
155
+ // body[3]=locked, body[4]=prog-disabled
156
+ const payload = {
157
+ type: 'sub_status',
158
+ channels: activeFromBitmask(body[1]),
159
+ enabled: activeFromBitmask(body[2]),
160
+ locked: activeFromBitmask(body[3]),
161
+ progDisabled: activeFromBitmask(body[4]),
162
+ };
163
+ node.send([{ payload }, null]);
164
+ return;
165
+ }
166
+ // Primary address — 8 bytes
167
+ // body[0]=0xED, body[1]=channel status, body[2]=enabled/disabled,
168
+ // body[3]=normal/inverted, body[4]=locked, body[5]=prog-disabled,
169
+ // body[6]=alarm&program, body[7]=auto-send interval
170
+ const progByte = body[6];
171
+ const program = ['none', 'group1', 'group2', 'group3'][progByte & 0x03];
172
+ const alarm1Active = !!(progByte & 0x04);
173
+ const alarm2Active = !!(progByte & 0x10);
174
+ const sunriseEnabled = !!(progByte & 0x40);
175
+ const sunsetEnabled = !!(progByte & 0x80);
176
+
177
+ const on = body[1] !== 0;
178
+ const payload = {
179
+ type: 'status',
180
+ on,
181
+ channels: activeFromBitmask(body[1]),
182
+ enabled: activeFromBitmask(body[2]),
183
+ normal: activeFromBitmask(body[3]),
184
+ locked: activeFromBitmask(body[4]),
185
+ progDisabled: activeFromBitmask(body[5]),
186
+ program,
187
+ alarms: { alarm1Active, alarm2Active },
188
+ sunrise: sunriseEnabled,
189
+ sunset: sunsetEnabled,
190
+ autoSendInterval: body[7],
191
+ };
192
+ node.send([{ payload }, null]);
193
+ break;
194
+ }
195
+
196
+ // ── 0xA4 Energy counter value ────────────────────────────────────
197
+ case 0xA4: {
198
+ if (body.length < 8) return;
199
+ // body[0]=0xA4
200
+ // body[1]: bits4-7=channel(0-7), bits0-3=power high nibble
201
+ // body[2-3]: power low bytes → 20-bit total in Watts
202
+ // body[4-7]: 32-bit energy in Wh/litres/ml
203
+ const channel = ((body[1] >> 4) & 0x0F) + 1; // 1-8
204
+ const powerHi = body[1] & 0x0F; // bits 19-16
205
+ const powerW = (powerHi << 16) | (body[2] << 8) | body[3]; // 20-bit
206
+ const energyWh = ((body[4] << 24) | (body[5] << 16) |
207
+ (body[6] << 8) | body[7]) >>> 0;
208
+
209
+ const payload = {
210
+ type: 'energy',
211
+ channel,
212
+ powerW,
213
+ energyWh,
214
+ };
215
+ node.send([null, { payload }]);
216
+ break;
217
+ }
218
+
219
+ // ── 0xF0/F1/F2 Channel name parts ───────────────────────────────
220
+ case 0xF0:
221
+ case 0xF1:
222
+ case 0xF2: {
223
+ // body[0]=cmd, body[1]=channel number (1-32), body[2..]=chars
224
+ const ch = body[1];
225
+ const part = cmd - 0xF0;
226
+ if (!_nameParts[ch]) _nameParts[ch] = {};
227
+ _nameParts[ch][part] = Array.from(body).slice(2);
228
+ // Assemble only channel 1 name for module display name
229
+ if (ch === 1 && part === 2) assembleName();
230
+ break;
231
+ }
232
+
233
+ default:
234
+ break;
235
+ }
236
+ }
237
+
238
+ node.bridge.register(node.address, onPacket);
239
+
240
+ // Startup RTR
241
+ setTimeout(function() {
242
+ node.bridge.send(rtrPkt(node.address), true);
243
+ }, 500);
244
+
245
+ // ── Input commands ────────────────────────────────────────────────────
246
+
247
+ node.on('input', function(msg) {
248
+ const cmd = msg.payload && msg.payload.cmd;
249
+ if (!cmd) return;
250
+
251
+ if (cmd === 'get_status') {
252
+ node.bridge.send(pkt(0xFB, node.address, [0xFA, 0x00]));
253
+ return;
254
+ }
255
+
256
+ // Request energy counter — DB2=channel bitmask (ch1-8), DB3=interval
257
+ if (cmd === 'get_counter') {
258
+ const chMask = parseInt(msg.payload.channels) || 0xFF;
259
+ const interval = parseInt(msg.payload.interval) || 0;
260
+ node.bridge.send(pkt(0xF8, node.address, [0xBD, chMask, interval]));
261
+ return;
262
+ }
263
+
264
+ // Load counter — DB2=channel number (0-7), DB3=don't care, DB4-7=32-bit value
265
+ if (cmd === 'load_counter') {
266
+ const ch = (parseInt(msg.payload.channel) || 1) - 1; // 0-indexed
267
+ const value = parseInt(msg.payload.value) || 0;
268
+ const b4 = (value >>> 24) & 0xFF;
269
+ const b5 = (value >>> 16) & 0xFF;
270
+ const b6 = (value >>> 8) & 0xFF;
271
+ const b7 = value & 0xFF;
272
+ node.bridge.send(pkt(0xF8, node.address, [0xAD, ch, 0x00, b4, b5, b6, b7]));
273
+ return;
274
+ }
275
+
276
+ // Lock channel — DB2=channel number (1-32, 0xFF=all), 24-bit duration
277
+ if (cmd === 'lock') {
278
+ const ch = parseInt(msg.payload.channel) || 0xFF;
279
+ const duration = parseInt(msg.payload.duration) || 0xFFFFFF;
280
+ const hi = (duration >> 16) & 0xFF;
281
+ const mid = (duration >> 8) & 0xFF;
282
+ const lo = duration & 0xFF;
283
+ node.bridge.send(pkt(0xF8, node.address, [0x12, ch, hi, mid, lo]));
284
+ return;
285
+ }
286
+
287
+ if (cmd === 'unlock') {
288
+ const ch = parseInt(msg.payload.channel) || 0xFF;
289
+ node.bridge.send(pkt(0xF8, node.address, [0x13, ch]));
290
+ return;
291
+ }
292
+
293
+ // Request channel name — DB2=channel number (1-32, 0xFF=all)
294
+ if (cmd === 'get_name') {
295
+ const ch = parseInt(msg.payload.channel) || 0xFF;
296
+ node.bridge.send(pkt(0xF8, node.address, [0xEF, ch]));
297
+ return;
298
+ }
299
+
300
+ node.warn('velbus-sensor-20: unknown cmd: ' + cmd);
301
+ });
302
+
303
+ // ── Cleanup ───────────────────────────────────────────────────────────
304
+
305
+ node.on('close', function() {
306
+ if (_nameTimer) { clearTimeout(_nameTimer); _nameTimer = null; }
307
+ node.bridge.deregister(node.address, onPacket);
308
+ });
309
+
310
+ setStatus('ready', 'grey', 'dot');
311
+ }
312
+
313
+ RED.nodes.registerType('velbus-sensor-20', VelbusSensor20Node);
314
+ };
@@ -0,0 +1,191 @@
1
+ <script type="text/javascript">
2
+ (function() {
3
+
4
+ RED.nodes.registerType('velbus-thermostat', {
5
+ category: 'Velbus (inputs)',
6
+ color: '#3A8C8C',
7
+ defaults: {
8
+ name: { value: '' },
9
+ bridge: { value: '', type: 'velbus-bridge', required: true },
10
+ address: { value: '', required: true, validate: function(v) { return /^(0x)?[0-9a-fA-F]{1,2}$/.test(v); } },
11
+ moduleName: { value: '' },
12
+ },
13
+ inputs: 1,
14
+ outputs: 2,
15
+ outputLabels: ['thermostat / settings / programme', 'temperature'],
16
+ icon: 'font-awesome/fa-thermometer-half',
17
+ paletteLabel: 'thermostat',
18
+
19
+ label: function() {
20
+ return this.name || this.moduleName
21
+ ? (this.name || this.moduleName) + ' thermostat'
22
+ : 'thermostat 0x' + (parseInt(this.address) || 0).toString(16).padStart(2, '0').toUpperCase();
23
+ },
24
+
25
+ oneditprepare: function() {
26
+ const self = this;
27
+ const savedAddr = this.address;
28
+
29
+ function populateDropdown(bridgeId) {
30
+ const $addr = $('#node-input-address');
31
+ if (!bridgeId) return;
32
+
33
+ $.getJSON('velbus/scan-results?bridge=' + bridgeId, function(results) {
34
+ const modules = (results && results.modules) ? results.modules : [];
35
+ if (!modules.length) return;
36
+
37
+ $addr.empty().append($('<option>').val('').text('— select address —'));
38
+
39
+ const GP_TYPE_PREFIXES = [
40
+ 0x21, 0x2D, 0x34, 0x35, 0x36, 0x37, 0x38, 0x47,
41
+ 0x3A, 0x3B, 0x3C, 0x3D,
42
+ 0x4F, 0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56,
43
+ 0x57, 0x5C, 0x5F,
44
+ 0x32, // VMB4AN also has thermostat preset model
45
+ ];
46
+
47
+ const gpModules = modules.filter(m => {
48
+ const tid = parseInt(m.typeId, 16);
49
+ return GP_TYPE_PREFIXES.includes(tid);
50
+ });
51
+
52
+ if (!gpModules.length) {
53
+ $addr.append($('<option>').val('').text('No thermostat modules found'));
54
+ return;
55
+ }
56
+
57
+ gpModules.forEach(function(m) {
58
+ const label = m.address + ' — ' + m.module;
59
+ const $opt = $('<option>').val(m.addressDec).text(label);
60
+ if (m.addressDec === parseInt(savedAddr)) $opt.attr('selected', true);
61
+ $addr.append($opt);
62
+ });
63
+ });
64
+ }
65
+
66
+ $('#node-input-bridge').on('change', function() {
67
+ populateDropdown($(this).val());
68
+ });
69
+
70
+ // Deferred load — TypedInput sets value async after oneditprepare
71
+
72
+
73
+ $('#node-input-bridge').trigger('change');
74
+
75
+
76
+ setTimeout(function() { $('#node-input-bridge').trigger('change'); }, 150);
77
+ },
78
+
79
+ oneditsave: function() {
80
+ this.address = parseInt($('#node-input-address').val()) || 0;
81
+ },
82
+ });
83
+
84
+ })();
85
+ </script>
86
+
87
+ <script type="text/html" data-template-name="velbus-thermostat">
88
+ <div class="form-row">
89
+ <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
90
+ <input type="text" id="node-input-name" placeholder="Optional label">
91
+ </div>
92
+
93
+ <div class="form-row">
94
+ <label for="node-input-bridge"><i class="fa fa-plug"></i> Bridge</label>
95
+ <input type="text" id="node-input-bridge">
96
+ </div>
97
+
98
+ <div class="form-row">
99
+ <label for="node-input-address"><i class="fa fa-map-marker"></i> Address</label>
100
+ <select id="node-input-address" style="width:70%">
101
+ <option value="">— select bridge first —</option>
102
+ </select>
103
+ </div>
104
+
105
+ <div class="form-row" style="display:none">
106
+ <input type="text" id="node-input-moduleName">
107
+ </div>
108
+
109
+ <p style="margin-top:12px; color:#888; font-size:0.9em; padding-left:110px;">
110
+ Target the primary module address — not the thermostat sub-address.<br>
111
+ Can share an address with a <strong>velbus-glass-panel</strong> node.
112
+ </p>
113
+ </script>
114
+
115
+ <script type="text/html" data-help-name="velbus-thermostat">
116
+ <p style="background:#fff3cd; border-left:4px solid #ffc107; padding:8px 12px; margin-bottom:12px; font-size:0.85em;">
117
+ <strong>&#9888; Testing status:</strong> This node was generated with Claude.ai and is in
118
+ need of extensive field testing before being deployed into a commercial project.
119
+ It is presented "as is" &mdash; any use beyond testing is entirely at your own risk and liability.
120
+ Constructive feedback is welcomed, accompanied by as many examples and debug captures as possible.
121
+ <a href="https://github.com/MDAR/node-red-contrib-velbus-2026/issues" target="_blank">File an issue on GitHub</a>.
122
+ </p>
123
+ <p>
124
+ Dedicated thermostat control node for all Velbus glass panel modules — VMBEL, VMBGP,
125
+ VMBELO and VMBGPO families, original and V2 series.
126
+ For button press events from the same module use a <strong>velbus-glass-panel</strong> node
127
+ on the same address.
128
+ </p>
129
+ <p>
130
+ All commands go to the <strong>primary module address</strong>. The thermostat sub-address
131
+ is a source address for button events only and is handled transparently by the bridge.
132
+ </p>
133
+
134
+ <h3>Output 1 — Thermostat and settings</h3>
135
+ <p>Thermostat status (0xEA):</p>
136
+ <pre>{
137
+ "type": "thermostat",
138
+ "currentTemp": 21.5,
139
+ "targetTemp": 22.0,
140
+ "mode": "comfort", // comfort | day | night | safe
141
+ "heaterMode": true, // true=heat, false=cool
142
+ "heating": false, // actively heating
143
+ "cooling": false, // actively cooling
144
+ "boostMode": false,
145
+ "thermostatOn": true
146
+ }</pre>
147
+ <p>Temperature preset settings (0xE8) — on <code>get_settings</code> command:</p>
148
+ <pre>{
149
+ "type": "temp_settings",
150
+ "comfort": 22.0,
151
+ "day": 20.0,
152
+ "night": 18.0,
153
+ "safe": 12.0
154
+ }</pre>
155
+ <p>Programme status (0xED) — on <code>get_status</code> command:</p>
156
+ <pre>{
157
+ "type": "programme",
158
+ "thermostatProgram": "manual", // disabled | manual | timer | timer_override
159
+ "alarms": { "alarm1Active": false, "alarm2Active": false }
160
+ }</pre>
161
+
162
+ <h3>Output 2 — Temperature</h3>
163
+ <pre>{ "type": "temperature", "current": 21.5, "min": 15.0, "max": 30.0 }</pre>
164
+
165
+ <h3>Input commands</h3>
166
+ <dl>
167
+ <dt><code>{ "cmd": "comfort" }</code> / <code>"day"</code> / <code>"night"</code> / <code>"safe"</code></dt>
168
+ <dd>Switch thermostat mode. Optional <code>sleepTime</code> in minutes (0 = until next programme step).</dd>
169
+
170
+ <dt><code>{ "cmd": "set_temp", "pointer": 0, "temp": 21.5 }</code></dt>
171
+ <dd>Set a preset target temperature. pointer: 0=comfort, 1=day, 2=night, 3=safe.</dd>
172
+
173
+ <dt><code>{ "cmd": "get_thermostat" }</code></dt>
174
+ <dd>Request current thermostat status (triggers 0xEA response on output 1).</dd>
175
+
176
+ <dt><code>{ "cmd": "get_settings" }</code></dt>
177
+ <dd>Request all four preset temperatures (triggers 0xE8 response on output 1).</dd>
178
+
179
+ <dt><code>{ "cmd": "get_status" }</code></dt>
180
+ <dd>Request module status — returns programme mode and alarm state on output 1.</dd>
181
+
182
+ <dt><code>{ "cmd": "heat_mode" }</code> / <code>{ "cmd": "cool_mode" }</code></dt>
183
+ <dd>Switch between heating and cooling mode.</dd>
184
+ </dl>
185
+
186
+ <h3>Supported modules</h3>
187
+ <p>
188
+ All VMBEL, VMBGP, VMBELO and VMBGPO variants (original and V2 series).<br>
189
+ VMB4AN (shares the same 4-preset thermostat model).
190
+ </p>
191
+ </script>