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,273 @@
1
+ 'use strict';
2
+
3
+ const { pkt } = require('../../lib/velbus-utils');
4
+
5
+ // ─────────────────────────────────────────────────────────────────────────────
6
+ // Velbus clock broadcast node
7
+ //
8
+ // Sends system time (0xD8), date (0xB7), daylight-saving flag (0xAF), and
9
+ // wake/sleep clock alarms (0xC3) to the bus. Time/date/DST are broadcast-only
10
+ // (address 0x00) — every RTC-equipped module receives and updates its own
11
+ // clock from these. This is what a physical Signum master clock module
12
+ // normally does; this node lets Node-RED (or the host it runs on) act as
13
+ // that time source instead, or as a backup if no Signum clock is fitted.
14
+ //
15
+ // All commands confirmed against protocol_vmb4ryld_20_vmb4ryno_20_vmb1rys_20.pdf
16
+ // (ed.3), and cross-checked identical in protocol_vmb8dc_20.pdf and
17
+ // protocol_vmbgp1_20_vmbgp2_20_vmbgp4_20.pdf — this is a shared V2-only
18
+ // "system clock" command block, confirmed absent entirely from the
19
+ // original-series protocol PDFs. Every V2 module type implements it
20
+ // identically; it is not tied to any one module's semantics.
21
+ //
22
+ // 0xD8 Set real time clock — DLC=4, body=[0xD8, dayOfWeek, hours, minutes]
23
+ // broadcast (0x00) ONLY — no local variant exists.
24
+ // 0xB7 Set date — DLC=5, body=[0xB7, day, month, yearHi, yearLo]
25
+ // broadcast (0x00) ONLY — no local variant exists.
26
+ // 0xAF Set daylight savings — DLC=2, body=[0xAF, 0|1]
27
+ // broadcast (0x00) ONLY — no local variant exists.
28
+ // 0xC3 Set clock alarm — DLC=7, body=[0xC3, alarm(1|2), wakeHour,
29
+ // wakeMinute, bedHour, bedMinute, enable(0|1)]
30
+ // IDENTICAL body whether "global" (address
31
+ // 0x00, every module gets the same alarm) or
32
+ // "local" (a specific module address, that
33
+ // module only) — the protocol PDF literally
34
+ // defines these as two address variants of
35
+ // the same command, not two different ones.
36
+ //
37
+ // dayOfWeek: 0=Monday...6=Sunday (NOT JS Date's 0=Sunday...6=Saturday).
38
+ // 0x00 is broadcast-only — never assign it as a real module's own address.
39
+ //
40
+ // DELIBERATE EXCEPTION to this palette's usual "one node = one physical
41
+ // module, fixed configured address" pattern: set_alarm accepts an optional
42
+ // per-message address override (msg.payload.address). This is intentional,
43
+ // not an oversight — 0xC3 is a bus-wide system command any V2 module can
44
+ // receive, not a per-module-type feature like an RGBW device-type setting
45
+ // or a thermostat setpoint. Putting "local alarm" support on every relevant
46
+ // node type instead (velbus-relay-20, velbus-dimmer-20, velbus-glass-panel,
47
+ // etc.) would mean duplicating an identical handler six times over for a
48
+ // command that is, by the protocol's own design, address-scoped rather than
49
+ // module-type-scoped. If this turns out to want per-module config instead
50
+ // (e.g. a dedicated address field with scan-dropdown support, matching every
51
+ // other node), that's a reasonable direction to revisit — this was a
52
+ // judgement call, not a settled architecture decision.
53
+ // ─────────────────────────────────────────────────────────────────────────────
54
+
55
+ const BROADCAST_ADDR = 0x00;
56
+
57
+ function velbusDow(jsDay) {
58
+ // JS Date#getDay(): 0=Sunday...6=Saturday. Velbus: 0=Monday...6=Sunday.
59
+ return (jsDay + 6) % 7;
60
+ }
61
+
62
+ // Best-effort DST detection from the host's own configured timezone.
63
+ // Standard trick: compare the current UTC offset against the year's two
64
+ // solstice-adjacent offsets (Jan 1 / Jul 1). Correct for any timezone that
65
+ // observes DST with a single annual transition (UK/EU/US style). Assumes
66
+ // the host machine's TZ is set correctly (e.g. Europe/London) — if the
67
+ // Node-RED host's clock/timezone is wrong, this will be wrong too.
68
+ function isDstActive(d) {
69
+ const jan = new Date(d.getFullYear(), 0, 1);
70
+ const jul = new Date(d.getFullYear(), 6, 1);
71
+ const stdOffset = Math.max(jan.getTimezoneOffset(), jul.getTimezoneOffset());
72
+ return d.getTimezoneOffset() < stdOffset;
73
+ }
74
+
75
+ function clampInt(v, min, max, fallback) {
76
+ const n = parseInt(v);
77
+ if (isNaN(n)) return fallback;
78
+ return Math.min(max, Math.max(min, n));
79
+ }
80
+
81
+ module.exports = function(RED) {
82
+
83
+ function VelbusClockNode(config) {
84
+ RED.nodes.createNode(this, config);
85
+ const node = this;
86
+
87
+ node.bridge = RED.nodes.getNode(config.bridge);
88
+ node.autoBroadcast = !!config.autoBroadcast;
89
+ node.intervalMinutes = parseInt(config.intervalMinutes) || 60;
90
+ node.includeDate = config.includeDate !== false;
91
+ node.includeDst = config.includeDst !== false;
92
+
93
+ if (!node.bridge) {
94
+ node.status({ fill: 'red', shape: 'ring', text: 'no bridge' });
95
+ node.error('velbus-clock: no bridge configured');
96
+ return;
97
+ }
98
+
99
+ let _intervalTimer = null;
100
+ let _startupTimer = null;
101
+
102
+ function setStatus(text, fill, shape) {
103
+ node.status({ fill: fill || 'green', shape: shape || 'dot', text });
104
+ }
105
+
106
+ // ── Packet senders ────────────────────────────────────────────────────
107
+
108
+ function sendTime(d) {
109
+ node.bridge.send(pkt(0xFB, BROADCAST_ADDR, [0xD8, velbusDow(d.getDay()), d.getHours(), d.getMinutes()]));
110
+ }
111
+
112
+ function sendDate(d) {
113
+ const year = d.getFullYear();
114
+ node.bridge.send(pkt(0xFB, BROADCAST_ADDR, [0xB7, d.getDate(), d.getMonth() + 1, (year >> 8) & 0xFF, year & 0xFF]));
115
+ }
116
+
117
+ function sendDst(active) {
118
+ node.bridge.send(pkt(0xFB, BROADCAST_ADDR, [0xAF, active ? 1 : 0]));
119
+ }
120
+
121
+ function sendAlarm(targetAddr, alarmNum, wakeHour, wakeMinute, bedHour, bedMinute, enabled) {
122
+ node.bridge.send(pkt(0xFB, targetAddr,
123
+ [0xC3, alarmNum, wakeHour, wakeMinute, bedHour, bedMinute, enabled ? 1 : 0]));
124
+ }
125
+
126
+ // Resolves msg.payload.address to a target address for set_alarm.
127
+ // Omitted/empty → broadcast (global alarm, every module). A valid
128
+ // 1-254 address (decimal or hex string) → that module only (local alarm).
129
+ function resolveTargetAddr(addrIn) {
130
+ if (addrIn === undefined || addrIn === null || addrIn === '') {
131
+ return { addr: BROADCAST_ADDR, label: 'global (0x00)' };
132
+ }
133
+ const a = (typeof addrIn === 'number') ? addrIn : parseInt(addrIn, 16);
134
+ if (isNaN(a) || a < 1 || a > 254) {
135
+ node.warn('velbus-clock: invalid address "' + addrIn + '" for set_alarm — sending globally instead');
136
+ return { addr: BROADCAST_ADDR, label: 'global (0x00)' };
137
+ }
138
+ return { addr: a, label: 'local (0x' + a.toString(16).padStart(2, '0').toUpperCase() + ')' };
139
+ }
140
+
141
+ function broadcastAll(d, opts) {
142
+ if (!node.bridge.isConnected()) {
143
+ node.warn('velbus-clock: bridge not connected, broadcast skipped');
144
+ setStatus('not connected', 'red');
145
+ return;
146
+ }
147
+
148
+ sendTime(d);
149
+ if (opts.date) sendDate(d);
150
+
151
+ let dst = null;
152
+ if (opts.includeDst) {
153
+ dst = (typeof opts.dstOverride === 'boolean') ? opts.dstOverride : isDstActive(d);
154
+ sendDst(dst);
155
+ }
156
+
157
+ setStatus('sent ' + d.toTimeString().slice(0, 8) +
158
+ (opts.date ? ' + date' : '') +
159
+ (opts.includeDst ? (' + DST:' + (dst ? 'on' : 'off')) : ''), 'green');
160
+
161
+ node.send({
162
+ payload: {
163
+ topic: 'clock_broadcast',
164
+ dayOfWeek: velbusDow(d.getDay()),
165
+ hours: d.getHours(),
166
+ minutes: d.getMinutes(),
167
+ date: opts.date ? { day: d.getDate(), month: d.getMonth() + 1, year: d.getFullYear() } : null,
168
+ dst,
169
+ timestamp: d.getTime()
170
+ }
171
+ });
172
+ }
173
+
174
+ // ── Input ─────────────────────────────────────────────────────────────
175
+ // Bare inject (empty payload) triggers a full broadcast using node
176
+ // config defaults — no msg shaping required for the common case.
177
+
178
+ node.on('input', function(msg) {
179
+ const inp = (msg && msg.payload && typeof msg.payload === 'object') ? msg.payload : {};
180
+ const cmd = inp.cmd || 'broadcast';
181
+
182
+ let when = inp.date ? new Date(inp.date) : new Date();
183
+ if (isNaN(when.getTime())) {
184
+ node.warn('velbus-clock: invalid msg.payload.date, using current time instead');
185
+ when = new Date();
186
+ }
187
+
188
+ switch (cmd) {
189
+ case 'broadcast':
190
+ broadcastAll(when, { date: node.includeDate, includeDst: node.includeDst, dstOverride: inp.dst });
191
+ break;
192
+
193
+ case 'broadcast_time':
194
+ if (!node.bridge.isConnected()) { node.warn('velbus-clock: bridge not connected'); return; }
195
+ sendTime(when);
196
+ setStatus('sent time ' + when.toTimeString().slice(0, 8), 'green');
197
+ break;
198
+
199
+ case 'broadcast_date':
200
+ if (!node.bridge.isConnected()) { node.warn('velbus-clock: bridge not connected'); return; }
201
+ sendDate(when);
202
+ setStatus('sent date ' + when.toDateString(), 'green');
203
+ break;
204
+
205
+ case 'broadcast_dst': {
206
+ if (!node.bridge.isConnected()) { node.warn('velbus-clock: bridge not connected'); return; }
207
+ const dst = (typeof inp.dst === 'boolean') ? inp.dst : isDstActive(when);
208
+ sendDst(dst);
209
+ setStatus('sent DST:' + (dst ? 'on' : 'off'), 'green');
210
+ break;
211
+ }
212
+
213
+ case 'set_alarm': {
214
+ const alarmNum = parseInt(inp.alarm);
215
+ if (alarmNum !== 1 && alarmNum !== 2) {
216
+ node.warn('velbus-clock: set_alarm requires "alarm": 1 or 2');
217
+ return;
218
+ }
219
+ if (!node.bridge.isConnected()) { node.warn('velbus-clock: bridge not connected'); return; }
220
+
221
+ const wakeHour = clampInt(inp.wakeHour, 0, 23, 0);
222
+ const wakeMinute = clampInt(inp.wakeMinute, 0, 59, 0);
223
+ const bedHour = clampInt(inp.bedHour, 0, 23, 0);
224
+ const bedMinute = clampInt(inp.bedMinute, 0, 59, 0);
225
+ const enabled = inp.enabled !== false; // default true — set_alarm implies enabling it
226
+
227
+ const { addr: targetAddr, label } = resolveTargetAddr(inp.address);
228
+
229
+ sendAlarm(targetAddr, alarmNum, wakeHour, wakeMinute, bedHour, bedMinute, enabled);
230
+
231
+ setStatus('alarm ' + alarmNum + ' ' + (enabled ? 'set' : 'disabled') + ' — ' + label, 'green');
232
+
233
+ node.send({
234
+ payload: {
235
+ topic: 'alarm_set',
236
+ target: targetAddr === BROADCAST_ADDR ? 'global' : ('0x' + targetAddr.toString(16).padStart(2, '0').toUpperCase()),
237
+ alarm: alarmNum,
238
+ wakeHour, wakeMinute, bedHour, bedMinute,
239
+ enabled
240
+ }
241
+ });
242
+ break;
243
+ }
244
+
245
+ default:
246
+ node.warn('velbus-clock: unknown cmd "' + cmd + '"');
247
+ }
248
+ });
249
+
250
+ // ── Auto broadcast ────────────────────────────────────────────────────
251
+
252
+ if (node.autoBroadcast) {
253
+ const intervalMs = Math.max(1, node.intervalMinutes) * 60000;
254
+
255
+ function tick() {
256
+ broadcastAll(new Date(), { date: node.includeDate, includeDst: node.includeDst });
257
+ }
258
+
259
+ // Delayed first send — give the bridge a moment to connect after deploy.
260
+ _startupTimer = setTimeout(tick, 5000);
261
+ _intervalTimer = setInterval(tick, intervalMs);
262
+ }
263
+
264
+ node.on('close', function() {
265
+ if (_startupTimer) clearTimeout(_startupTimer);
266
+ if (_intervalTimer) clearInterval(_intervalTimer);
267
+ });
268
+
269
+ setStatus('ready', 'grey');
270
+ }
271
+
272
+ RED.nodes.registerType('velbus-clock', VelbusClockNode);
273
+ };
@@ -0,0 +1,174 @@
1
+ <script type="text/javascript">
2
+
3
+ // ── Shared address dropdown helper ──────────────────────────────────────────
4
+ function velbusPopulateAddressDropdown(nodeCtx, bridgeSelector, addrSelector, nodeTypes) {
5
+ var addrField = $(addrSelector);
6
+ var currentVal = String(addrField.val() || nodeCtx.address || nodeCtx.moduleAddr || '').replace('0x','').toUpperCase();
7
+
8
+ function buildDropdown(modules) {
9
+ var filtered = nodeTypes
10
+ ? modules.filter(function(m) { return nodeTypes.indexOf(m.suggestedNode) !== -1; })
11
+ : modules;
12
+
13
+ if (filtered.length === 0) {
14
+ addrField.show();
15
+ return;
16
+ }
17
+
18
+ var select = $('<select>', { id: addrSelector.replace('#',''), style: 'width:100%' });
19
+ select.append($('<option>', { value: '', text: '\u2014 select or type address \u2014' }));
20
+
21
+ filtered.forEach(function(m) {
22
+ var addrHex = m.address.replace('0x','').toUpperCase();
23
+ var label = m.address + ' ' + m.module +
24
+ (m.velbusName && m.velbusName !== m.module ? ' "' + m.velbusName + '"' : '') +
25
+ ' (build ' + m.build + ', map v' + m.memoryMapVersion + ')';
26
+ var opt = $('<option>', { value: addrHex, text: label });
27
+ if (addrHex === currentVal) opt.prop('selected', true);
28
+ select.append(opt);
29
+ opt.data('module', m);
30
+ });
31
+
32
+ select.append($('<option>', { value: '__manual__', text: '\u2014 enter address manually \u2014' }));
33
+ addrField.replaceWith(select);
34
+
35
+ select.on('change', function() {
36
+ if ($(this).val() === '__manual__') {
37
+ var input = $('<input>', {
38
+ type: 'text', id: addrSelector.replace('#',''),
39
+ placeholder: 'hex e.g. A0', style: 'width:80px'
40
+ });
41
+ $(this).replaceWith(input);
42
+ return;
43
+ }
44
+ var m = $(this).find(':selected').data('module');
45
+ if (m && m.channels) {
46
+ var cc = $('#node-input-channelCount');
47
+ if (cc.length) cc.val(m.channels);
48
+ }
49
+ });
50
+ }
51
+
52
+ function loadResults() {
53
+ var bridgeId = $(bridgeSelector).val();
54
+ if (!bridgeId) return;
55
+ $.getJSON('velbus/scan-results?bridge=' + bridgeId, function(data) {
56
+ if (data && data.modules && data.modules.length > 0) buildDropdown(data.modules);
57
+ }).fail(function() {});
58
+ }
59
+
60
+ $(bridgeSelector).on('change', loadResults);
61
+ setTimeout(function() { $(bridgeSelector).trigger('change'); }, 150);
62
+ }
63
+
64
+ RED.nodes.registerType('velbus-dimmer', {
65
+ category: 'Velbus (outputs)',
66
+ color: '#4A90D9',
67
+ defaults: {
68
+ name: { value: '' },
69
+ bridge: { value: '', type: 'velbus-bridge', required: true },
70
+ moduleAddr: { value: '', required: true },
71
+ startChannel: { value: 1, required: true, validate: RED.validators.number() },
72
+ channelCount: { value: 1, required: true, validate: RED.validators.number() }
73
+ },
74
+ inputs: 1,
75
+ outputs: 2,
76
+ icon: 'font-awesome/fa-sliders',
77
+ label: function() {
78
+ if (this.name) return this.name;
79
+ return 'dimmer 0x' + (this.moduleAddr || '??');
80
+ },
81
+ outputLabels: ['state / events', 'warnings'],
82
+ oneditprepare: function() {
83
+ velbusPopulateAddressDropdown(
84
+ this,
85
+ '#node-input-bridge',
86
+ '#node-input-moduleAddr',
87
+ ['velbus-dimmer']
88
+ );
89
+ }
90
+ });
91
+ </script>
92
+
93
+ <script type="text/html" data-template-name="velbus-dimmer">
94
+ <div class="form-row">
95
+ <label for="node-input-name"><i class="fa fa-tag"></i> Name override</label>
96
+ <input type="text" id="node-input-name"
97
+ placeholder="Leave blank to use module name from VelbusLink">
98
+ </div>
99
+ <div class="form-row">
100
+ <label for="node-input-bridge"><i class="fa fa-server"></i> Bridge</label>
101
+ <input type="text" id="node-input-bridge">
102
+ </div>
103
+ <div class="form-row">
104
+ <label for="node-input-moduleAddr"><i class="fa fa-microchip"></i> Address</label>
105
+ <input type="text" id="node-input-moduleAddr" placeholder="e.g. BF — run scan first for dropdown" style="width:100%">
106
+ <div class="form-tips" style="margin-top:4px">Run a <b>velbus-scan</b> node first to populate the dropdown with discovered modules.</div>
107
+ </div>
108
+ <div class="form-row">
109
+ <label for="node-input-startChannel"><i class="fa fa-list-ol"></i> Start channel</label>
110
+ <input type="number" id="node-input-startChannel" min="1" max="4" style="width:60px">
111
+ </div>
112
+ <div class="form-row">
113
+ <label for="node-input-channelCount"><i class="fa fa-hashtag"></i> Channel count</label>
114
+ <input type="number" id="node-input-channelCount" min="1" max="4" style="width:60px">
115
+ </div>
116
+ </script>
117
+
118
+ <script type="text/html" data-help-name="velbus-dimmer">
119
+ <p style="background:#fff3cd; border-left:4px solid #ffc107; padding:8px 12px; margin-bottom:12px; font-size:0.85em;">
120
+ <strong>&#9888; Testing status:</strong> This node was generated with Claude.ai and is in
121
+ need of extensive field testing before being deployed into a commercial project.
122
+ It is presented "as is" &mdash; any use beyond testing is entirely at your own risk and liability.
123
+ Constructive feedback is welcomed, accompanied by as many examples and debug captures as possible.
124
+ <a href="https://github.com/MDAR/node-red-contrib-velbus-2026/issues" target="_blank">File an issue on GitHub</a>.
125
+ </p>
126
+ <p><strong>Original series only (pre V2.0)</strong></p>
127
+ <p>For V2.0 dimmer modules (VMB2DC-20, VMB8DC-20, VMB4LEDPWM-20) use the <b>velbus-dimmer-20</b> node instead.</p>
128
+
129
+ <p>Supports: <b>VMBDMI</b> (1ch), <b>VMBDMI-R</b> (1ch), <b>VMB4DC</b> (4ch)</p>
130
+
131
+ <h3>Address dropdown</h3>
132
+ <p>Run a <b>velbus-scan</b> node first to populate the address dropdown.</p>
133
+
134
+ <h3>Output 1 — State / Events</h3>
135
+ <pre>
136
+ {
137
+ "topic": "dimmer_status",
138
+ "address": "0xBF",
139
+ "module": "Bathroom",
140
+ "channel": 1,
141
+ "state": "on",
142
+ "on": true,
143
+ "level": 75,
144
+ "percent": 75.0,
145
+ "timerRemaining": 0,
146
+ "thermal": {
147
+ "tempBand": 0,
148
+ "loadType": 0,
149
+ "error": 0
150
+ },
151
+ "timestamp": 1719100000000
152
+ }
153
+ </pre>
154
+
155
+ <h3>Input — Commands</h3>
156
+ <pre>
157
+ { "channel": 1, "cmd": "set", "level": 75 }
158
+ { "channel": 1, "cmd": "set", "percent": 75, "dimspeed": 3 }
159
+ { "channel": 1, "cmd": "on" }
160
+ { "channel": 1, "cmd": "off" }
161
+ { "channel": 1, "cmd": "restore" }
162
+ { "channel": 1, "cmd": "stop" }
163
+ { "channel": 1, "cmd": "timer", "duration": 30 }
164
+ { "channel": 1, "cmd": "forced_on", "duration": -1 }
165
+ { "channel": 1, "cmd": "forced_off", "duration": 3600 }
166
+ { "channel": 1, "cmd": "cancel_forced_on" }
167
+ { "channel": 1, "cmd": "cancel_forced_off" }
168
+ { "channel": 1, "cmd": "inhibit", "duration": 1800 }
169
+ { "channel": 1, "cmd": "cancel_inhibit" }
170
+ { "channel": 1, "cmd": "status" }
171
+ </pre>
172
+ <p>dimspeed in seconds (0=instant). Duration in seconds, -1=permanent.</p>
173
+ <p>VMB4DC: set <code>channel</code> to <code>"all"</code> for all channels simultaneously.</p>
174
+ </script>