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,337 @@
1
+ <script type="text/javascript">
2
+ (function() {
3
+
4
+ // ── Glass panel type list for info display ───────────────────────────────
5
+ const GLASS_PANEL_TYPES = {
6
+ 0x1E: { name: 'VMBGP1', channels: 1, oled: false, pir: false, series: 'original' },
7
+ 0x1F: { name: 'VMBGP2', channels: 2, oled: false, pir: false, series: 'original' },
8
+ 0x21: { name: 'VMBGPO', channels: 32, oled: true, pir: false, series: 'original' },
9
+ 0x2D: { name: 'VMBGP4PIR', channels: 8, oled: false, pir: true, series: 'original' },
10
+ 0x34: { name: 'VMBEL1', channels: 1, oled: false, pir: false, series: 'original' },
11
+ 0x35: { name: 'VMBEL2', channels: 2, oled: false, pir: false, series: 'original' },
12
+ 0x36: { name: 'VMBEL4', channels: 4, oled: false, pir: false, series: 'original' },
13
+ 0x37: { name: 'VMBELO', channels: 4, oled: true, pir: false, series: 'original' },
14
+ 0x38: { name: 'VMBELPIR', channels: 8, oled: false, pir: true, series: 'original' },
15
+ 0x3A: { name: 'VMBGP1-2', channels: 1, oled: false, pir: false, series: 'original' },
16
+ 0x3B: { name: 'VMBGP2-2', channels: 2, oled: false, pir: false, series: 'original' },
17
+ 0x3C: { name: 'VMBGP4-2', channels: 4, oled: false, pir: false, series: 'original' },
18
+ 0x3D: { name: 'VMBGPOD-2', channels: 4, oled: true, pir: false, series: 'original' },
19
+ 0x47: { name: 'VMBEL2PIR', channels: 6, oled: false, pir: true, series: 'original' },
20
+ 0x4F: { name: 'VMBEL1-20', channels: 1, oled: false, pir: false, series: 'v2' },
21
+ 0x50: { name: 'VMBEL2-20', channels: 2, oled: false, pir: false, series: 'v2' },
22
+ 0x51: { name: 'VMBEL4-20', channels: 4, oled: false, pir: false, series: 'v2' },
23
+ 0x52: { name: 'VMBELO-20', channels: 4, oled: true, pir: false, series: 'v2' },
24
+ 0x53: { name: 'VMBELPIR-20', channels: 8, oled: false, pir: true, series: 'v2' },
25
+ 0x54: { name: 'VMBGP1-20', channels: 1, oled: false, pir: false, series: 'v2' },
26
+ 0x55: { name: 'VMBGP2-20', channels: 2, oled: false, pir: false, series: 'v2' },
27
+ 0x56: { name: 'VMBGP4-20', channels: 4, oled: false, pir: false, series: 'v2' },
28
+ 0x57: { name: 'VMBGPO-20', channels: 32, oled: true, pir: false, series: 'v2' },
29
+ 0x5C: { name: 'VMBEL2PIR-20', channels: 6, oled: false, pir: true, series: 'v2' },
30
+ 0x5F: { name: 'VMBGP4PIR-20', channels: 8, oled: false, pir: true, series: 'v2' },
31
+ };
32
+
33
+ function populateAddressDropdown(bridgeId, selectedAddr, selectedTypeId) {
34
+ const $addr = $('#node-input-address');
35
+ if (!bridgeId) return;
36
+
37
+ $.getJSON('velbus/scan-results?bridge=' + bridgeId, function(results) {
38
+ const modules = (results && results.modules) ? results.modules : [];
39
+
40
+ const gpTypeIds = Object.keys(GLASS_PANEL_TYPES).map(k => parseInt(k));
41
+ const gpModules = modules.filter(function(m) {
42
+ return gpTypeIds.includes(parseInt(m.typeId, 16));
43
+ });
44
+
45
+ if (!gpModules.length) {
46
+ // No glass panels on bus — restore plain text input so user can type manually
47
+ if ($addr.is('select')) {
48
+ const input = $('<input>', {
49
+ type: 'text', id: 'node-input-address',
50
+ placeholder: 'hex address e.g. 0D', style: 'width:80px'
51
+ });
52
+ $addr.replaceWith(input);
53
+ }
54
+ return;
55
+ }
56
+
57
+ // Build select dropdown
58
+ const isSelect = $addr.is('select');
59
+ const $sel = isSelect ? $addr : $('<select>', { id: 'node-input-address', style: 'width:100%' });
60
+ $sel.empty();
61
+ $sel.append($('<option>').val('').text('\u2014 select address \u2014'));
62
+
63
+ gpModules.forEach(function(m) {
64
+ const addrDec = m.addressDec;
65
+ const tid = parseInt(m.typeId, 16);
66
+ const label = m.address + ' \u2014 ' + m.module +
67
+ (m.velbusName && m.velbusName !== m.module ? ' "' + m.velbusName + '"' : '') +
68
+ ' (build ' + m.build + ', map v' + m.memoryMapVersion + ')';
69
+ const $opt = $('<option>').val(addrDec).text(label).data('typeid', tid);
70
+ if (addrDec === parseInt(selectedAddr)) $opt.attr('selected', true);
71
+ $sel.append($opt);
72
+ });
73
+
74
+ // Manual entry fallback at bottom
75
+ $sel.append($('<option>').val('__manual__').text('\u2014 enter address manually \u2014'));
76
+
77
+ if (!isSelect) $addr.replaceWith($sel);
78
+
79
+ // Selecting manual entry swaps back to text input
80
+ $sel.on('change', function() {
81
+ if ($(this).val() === '__manual__') {
82
+ const input = $('<input>', {
83
+ type: 'text', id: 'node-input-address',
84
+ placeholder: 'hex e.g. 0D', style: 'width:80px'
85
+ });
86
+ $(this).replaceWith(input);
87
+ return;
88
+ }
89
+ const addrDec = parseInt($(this).val());
90
+ if (addrDec) updateTypeHint(addrDec, null);
91
+ });
92
+
93
+ // Pre-selected address — set hint
94
+ if (selectedAddr) updateTypeHint(parseInt(selectedAddr), selectedTypeId);
95
+ });
96
+ }
97
+
98
+ function updateTypeHint(addrDec, knownTypeId) {
99
+ const $hint = $('#gp-type-hint');
100
+ const $typeId = $('#node-input-typeId');
101
+ const $chanCol = $('#gp-channel-count');
102
+
103
+ // Find the selected option to get typeId
104
+ const $opt = $('#node-input-address option[value="' + addrDec + '"]');
105
+ const tid = $opt.data('typeid') || knownTypeId || null;
106
+
107
+ if (tid !== null && GLASS_PANEL_TYPES[tid]) {
108
+ const t = GLASS_PANEL_TYPES[tid];
109
+ $typeId.val('0x' + tid.toString(16).padStart(2, '0').toUpperCase());
110
+
111
+ const extras = [];
112
+ if (t.oled) extras.push('OLED');
113
+ if (t.pir) extras.push('PIR');
114
+
115
+ $hint.text(
116
+ t.name + ' — ' + t.channels + ' ch' +
117
+ (extras.length ? ' · ' + extras.join(', ') : '') +
118
+ ' · ' + t.series
119
+ ).show();
120
+
121
+ // Auto-fill channel count if field is empty or was auto-filled
122
+ const $ch = $('#node-input-channelCount');
123
+ if (!$ch.val() || $ch.data('auto')) {
124
+ $ch.val(t.channels).data('auto', true);
125
+ }
126
+ } else {
127
+ $hint.hide();
128
+ $typeId.val('');
129
+ }
130
+ }
131
+
132
+ RED.nodes.registerType('velbus-glass-panel', {
133
+ category: 'Velbus (inputs)',
134
+ color: '#3A8C8C',
135
+ defaults: {
136
+ name: { value: '' },
137
+ bridge: { value: '', type: 'velbus-bridge', required: true },
138
+ address: { value: '', required: true, validate: function(v) { return /^(0x)?[0-9a-fA-F]{1,2}$/.test(v); } },
139
+ channelCount: { value: 4, required: true, validate: RED.validators.number() },
140
+ moduleName: { value: '' },
141
+ typeId: { value: '' },
142
+ },
143
+ inputs: 1,
144
+ outputs: 3,
145
+ outputLabels: ['buttons', 'status / thermostat', 'name parts'],
146
+ icon: 'font-awesome/fa-th',
147
+ paletteLabel: 'glass panel',
148
+
149
+ label: function() {
150
+ return this.name || this.moduleName || ('GP 0x' +
151
+ (parseInt(this.address) || 0).toString(16).padStart(2, '0').toUpperCase());
152
+ },
153
+
154
+ oneditprepare: function() {
155
+ const self = this;
156
+ const savedAddr = this.address;
157
+ const savedTypeId = this.typeId ? parseInt(this.typeId, 16) : null;
158
+
159
+ // Type ID field is read-only — populated from scan results
160
+ $('#node-input-typeId').prop('readonly', true).css('background', '#f0f0f0');
161
+
162
+ // Bridge change → refresh dropdown
163
+ $('#node-input-bridge').on('change', function() {
164
+ const bridgeId = $(this).val();
165
+ populateAddressDropdown(bridgeId, savedAddr, savedTypeId);
166
+ });
167
+
168
+ // Address change → update type hint
169
+ $('#node-input-address').on('change', function() {
170
+ const addrDec = parseInt($(this).val());
171
+ if (addrDec) updateTypeHint(addrDec, null);
172
+ });
173
+
174
+ // Manual channel count edit marks field as no longer auto
175
+ $('#node-input-channelCount').on('input', function() {
176
+ $(this).data('auto', false);
177
+ });
178
+
179
+ // Initial load — deferred so TypedInput has time to render its value.
180
+ // .val() returns '' during oneditprepare; the TypedInput fires 'change'
181
+ // once it has set its value. We trigger that manually after a short delay
182
+ // as a reliable fallback.
183
+ setTimeout(function() {
184
+ $('#node-input-bridge').trigger('change');
185
+ }, 100);
186
+ },
187
+
188
+ oneditsave: function() {
189
+ // Store the address as decimal integer
190
+ const raw = $('#node-input-address').val();
191
+ this.address = parseInt(raw) || 0;
192
+
193
+ // Store typeId from hidden field
194
+ const tid = $('#node-input-typeId').val();
195
+ this.typeId = tid || '';
196
+
197
+ // Store module name from hint for status bar use
198
+ if (tid) {
199
+ const tidNum = parseInt(tid, 16);
200
+ const t = GLASS_PANEL_TYPES[tidNum];
201
+ if (t) this.moduleName = t.name;
202
+ }
203
+ },
204
+ });
205
+
206
+ })();
207
+ </script>
208
+
209
+ <script type="text/html" data-template-name="velbus-glass-panel">
210
+ <div class="form-row">
211
+ <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
212
+ <input type="text" id="node-input-name" placeholder="Optional label">
213
+ </div>
214
+
215
+ <div class="form-row">
216
+ <label for="node-input-bridge"><i class="fa fa-plug"></i> Bridge</label>
217
+ <input type="text" id="node-input-bridge">
218
+ </div>
219
+
220
+ <div class="form-row">
221
+ <label for="node-input-address"><i class="fa fa-map-marker"></i> Address</label>
222
+ <input type="text" id="node-input-address" placeholder="run scan first, or type hex address" style="width:70%">
223
+ </div>
224
+
225
+ <div class="form-row" id="gp-type-hint-row">
226
+ <label></label>
227
+ <span id="gp-type-hint" style="color:#888; font-size:0.9em; display:none;"></span>
228
+ </div>
229
+
230
+ <div class="form-row" style="display:none">
231
+ <input type="text" id="node-input-typeId">
232
+ <input type="text" id="node-input-moduleName">
233
+ </div>
234
+
235
+ <div class="form-row">
236
+ <label for="node-input-channelCount"><i class="fa fa-list-ol"></i> Channels</label>
237
+ <input type="number" id="node-input-channelCount" min="1" max="32" style="width:80px">
238
+ <span style="margin-left:8px; color:#888; font-size:0.9em;">auto-filled from scan</span>
239
+ </div>
240
+ </script>
241
+
242
+ <script type="text/html" data-help-name="velbus-glass-panel">
243
+ <p style="background:#fff3cd; border-left:4px solid #ffc107; padding:8px 12px; margin-bottom:12px; font-size:0.85em;">
244
+ <strong>&#9888; Testing status:</strong> This node was generated with Claude.ai and is in
245
+ need of extensive field testing before being deployed into a commercial project.
246
+ It is presented "as is" &mdash; any use beyond testing is entirely at your own risk and liability.
247
+ Constructive feedback is welcomed, accompanied by as many examples and debug captures as possible.
248
+ <a href="https://github.com/MDAR/node-red-contrib-velbus-2026/issues" target="_blank">File an issue on GitHub</a>.
249
+ </p>
250
+ <p>
251
+ Handles all Velbus glass panel and button modules — original and V2 series (VMBEL, VMBGP, VMBELO, VMBELPIR, VMBGPO, VMBGP4PIR families).
252
+ All modules include optional thermostat capability.
253
+ For relay and dimmer outputs see <strong>velbus-relay</strong> / <strong>velbus-dimmer</strong> nodes.
254
+ </p>
255
+
256
+ <h3>Outputs</h3>
257
+
258
+ <h4>Output 1 — Buttons</h4>
259
+ <p>Emitted on button press, release, or long-press (0x00):</p>
260
+ <pre>{
261
+ "type": "button",
262
+ "on": true,
263
+ "pressed": [1, 3],
264
+ "released": [2],
265
+ "longPressed": []
266
+ }</pre>
267
+ <p>For PIR variants (VMBELPIR, VMBGP4PIR etc.) channel numbers are augmented with semantic labels where known:</p>
268
+ <pre>pressed: [{ "channel": 6, "label": "dark" }]</pre>
269
+
270
+ <h4>Output 2 — Status and thermostat</h4>
271
+ <p>Module status (0xED):</p>
272
+ <pre>{
273
+ "type": "status",
274
+ "locked": [],
275
+ "thermostatProgram":"manual",
276
+ "alarms": { "alarm1Active": false, "alarm2Active": false, "selected": 0 },
277
+ "sunreel": 0,
278
+ "oledPage": 1 // OLED types only
279
+ }</pre>
280
+ <p>Thermostat status (0xEA):</p>
281
+ <pre>{
282
+ "type": "thermostat",
283
+ "currentTemp": 21.5,
284
+ "targetTemp": 22.0,
285
+ "mode": "comfort", // comfort | day | night | safe
286
+ "heaterMode": true,
287
+ "heating": false,
288
+ "cooling": false,
289
+ "boostMode": false,
290
+ "thermostatOn": true
291
+ }</pre>
292
+ <p>Temperature (0xE6):</p>
293
+ <pre>{ "type": "temperature", "current": 21.5, "min": 15.0, "max": 30.0 }</pre>
294
+ <p>Light sensor — PIR types only (0xA9):</p>
295
+ <pre>{ "type": "light", "raw": 1024 }</pre>
296
+ <p>Memo text — OLED types only (0xAC):</p>
297
+ <pre>{ "type": "memo", "page": 1, "text": "Hello" }</pre>
298
+ <p>Counter — OLED types only (0xBE):</p>
299
+ <pre>{ "type": "counter", "counter": 1, "value": 42 }</pre>
300
+
301
+ <h4>Output 3 — Name parts</h4>
302
+ <pre>{ "type": "name_part", "part": 0, "text": "Living" }</pre>
303
+ <p>Three parts (0, 1, 2) concatenate to form the full module name as stored in VelbusLink.</p>
304
+
305
+ <h3>Input commands</h3>
306
+ <dl>
307
+ <dt><code>{ "cmd": "comfort" }</code> / <code>"day"</code> / <code>"night"</code> / <code>"safe"</code></dt>
308
+ <dd>Switch thermostat mode. Optional <code>sleepTime</code> (minutes, 0 = until next programme step).</dd>
309
+
310
+ <dt><code>{ "cmd": "set_temp", "pointer": 0, "temp": 21.5 }</code></dt>
311
+ <dd>Set target temperature. pointer: 0=comfort, 1=day, 2=night, 3=safe.</dd>
312
+
313
+ <dt><code>{ "cmd": "led_set", "channel": 1 }</code></dt>
314
+ <dd>Control button LED. Commands: <code>led_set</code>, <code>led_clear</code>, <code>led_slow</code>, <code>led_fast</code>, <code>led_vfast</code>.</dd>
315
+
316
+ <dt><code>{ "cmd": "output_on" }</code> / <code>"output_off"</code></dt>
317
+ <dd>Open collector output on/off.</dd>
318
+
319
+ <dt><code>{ "cmd": "output_timer", "duration": 60 }</code></dt>
320
+ <dd>Open collector output timed on (seconds).</dd>
321
+
322
+ <dt><code>{ "cmd": "get_status" }</code></dt>
323
+ <dd>Request module status (triggers 0xED response).</dd>
324
+
325
+ <dt><code>{ "cmd": "get_thermostat" }</code></dt>
326
+ <dd>Request thermostat status (triggers 0xEA response).</dd>
327
+
328
+ <dt><code>{ "cmd": "get_name" }</code></dt>
329
+ <dd>Request module name (triggers three 0xF0/F1/F2 responses on output 3).</dd>
330
+ </dl>
331
+
332
+ <h3>Supported modules</h3>
333
+ <p>
334
+ "Original": VMBGP1, VMBGP2, VMBGPO, VMBGP4PIR, VMBEL1/2/4, VMBELO, VMBELPIR, VMBEL2PIR, VMBGP1/2/4-2, VMBGPOD-2.<br>
335
+ "V2": VMBEL1/2/4-20, VMBELO-20, VMBELPIR-20, VMBEL2PIR-20, VMBGP1/2/4-20, VMBGPO-20, VMBGP4PIR-20.
336
+ </p>
337
+ </script>