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,172 @@
1
+ <script type="text/javascript">
2
+ (function() {
3
+
4
+ const BUTTON_TYPES = {
5
+ 0x01: { name: 'VMB8PB', channels: 8, series: 'original' },
6
+ 0x16: { name: 'VMB8PBU', channels: 8, series: 'original' },
7
+ 0x17: { name: 'VMB6PBN', channels: 6, series: 'original' },
8
+ 0x1C: { name: 'VMB4PB', channels: 4, series: 'original' },
9
+ 0x20: { name: 'VMB6PB-20', channels: 6, series: 'v2' },
10
+ };
11
+
12
+ RED.nodes.registerType('velbus-button', {
13
+ category: 'Velbus (inputs)',
14
+ color: '#3A8C8C',
15
+ defaults: {
16
+ name: { value: '' },
17
+ bridge: { value: '', type: 'velbus-bridge', required: true },
18
+ address: { value: '', required: true, validate: function(v) { return /^(0x)?[0-9a-fA-F]{1,2}$/.test(v); } },
19
+ channelCount: { value: 8 },
20
+ moduleName: { value: '' },
21
+ typeId: { value: '' },
22
+ },
23
+ inputs: 0,
24
+ outputs: 1,
25
+ outputLabels: ['button events'],
26
+ icon: 'font-awesome/fa-hand-pointer-o',
27
+ paletteLabel: 'button',
28
+
29
+ label: function() {
30
+ return this.name || this.moduleName ||
31
+ ('BTN 0x' + (parseInt(this.address) || 0).toString(16).padStart(2, '0').toUpperCase());
32
+ },
33
+
34
+ oneditprepare: function() {
35
+ const savedAddr = this.address;
36
+ const savedTypeId = this.typeId ? parseInt(this.typeId, 16) : null;
37
+
38
+ $('#node-input-typeId').prop('readonly', true).css('background', '#f0f0f0');
39
+
40
+ function populateDropdown(bridgeId) {
41
+ const $addr = $('#node-input-address');
42
+ if (!bridgeId) return;
43
+
44
+ $.getJSON('velbus/scan-results?bridge=' + bridgeId, function(results) {
45
+ const modules = (results && results.modules) ? results.modules : [];
46
+ if (!modules.length) return;
47
+
48
+ $addr.empty().append($('<option>').val('').text('— select address —'));
49
+
50
+ const btnTypeIds = Object.keys(BUTTON_TYPES).map(k => parseInt(k));
51
+ const btnModules = modules.filter(m => btnTypeIds.includes(parseInt(m.typeId, 16)));
52
+
53
+ if (!btnModules.length) {
54
+ $addr.append($('<option>').val('').text('No button modules found on bus'));
55
+ return;
56
+ }
57
+
58
+ btnModules.forEach(function(m) {
59
+ const tid = parseInt(m.typeId, 16);
60
+ const label = m.address + ' — ' + m.module;
61
+ const $opt = $('<option>').val(m.addressDec).text(label).data('typeid', tid);
62
+ if (m.addressDec === parseInt(savedAddr)) $opt.attr('selected', true);
63
+ $addr.append($opt);
64
+ });
65
+
66
+ if (savedAddr) updateHint(parseInt(savedAddr));
67
+ });
68
+ }
69
+
70
+ function updateHint(addrDec) {
71
+ const $opt = $('#node-input-address option[value="' + addrDec + '"]');
72
+ const tid = $opt.data('typeid');
73
+ if (tid && BUTTON_TYPES[tid]) {
74
+ const t = BUTTON_TYPES[tid];
75
+ $('#btn-type-hint').text(t.name + ' — ' + t.channels + ' ch · ' + t.series).show();
76
+ $('#node-input-typeId').val('0x' + tid.toString(16).padStart(2,'0').toUpperCase());
77
+ const $ch = $('#node-input-channelCount');
78
+ if (!$ch.val() || $ch.data('auto')) $ch.val(t.channels).data('auto', true);
79
+ } else {
80
+ $('#btn-type-hint').hide();
81
+ }
82
+ }
83
+
84
+ $('#node-input-bridge').on('change', function() { populateDropdown($(this).val()); });
85
+ $('#node-input-address').on('change', function() { updateHint(parseInt($(this).val())); });
86
+ $('#node-input-channelCount').on('input', function() { $(this).data('auto', false); });
87
+
88
+ // Deferred load — TypedInput sets value async after oneditprepare
89
+
90
+
91
+ $('#node-input-bridge').trigger('change');
92
+
93
+
94
+ setTimeout(function() { $('#node-input-bridge').trigger('change'); }, 150);
95
+ },
96
+
97
+ oneditsave: function() {
98
+ this.address = parseInt($('#node-input-address').val()) || 0;
99
+ const tid = $('#node-input-typeId').val();
100
+ this.typeId = tid || '';
101
+ if (tid) {
102
+ const t = BUTTON_TYPES[parseInt(tid, 16)];
103
+ if (t) this.moduleName = t.name;
104
+ }
105
+ },
106
+ });
107
+
108
+ })();
109
+ </script>
110
+
111
+ <script type="text/html" data-template-name="velbus-button">
112
+ <div class="form-row">
113
+ <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
114
+ <input type="text" id="node-input-name" placeholder="Optional label">
115
+ </div>
116
+ <div class="form-row">
117
+ <label for="node-input-bridge"><i class="fa fa-plug"></i> Bridge</label>
118
+ <input type="text" id="node-input-bridge">
119
+ </div>
120
+ <div class="form-row">
121
+ <label for="node-input-address"><i class="fa fa-map-marker"></i> Address</label>
122
+ <select id="node-input-address" style="width:70%">
123
+ <option value="">— select bridge first —</option>
124
+ </select>
125
+ </div>
126
+ <div class="form-row">
127
+ <label></label>
128
+ <span id="btn-type-hint" style="color:#888; font-size:0.9em; display:none;"></span>
129
+ </div>
130
+ <div class="form-row" style="display:none">
131
+ <input type="text" id="node-input-typeId">
132
+ <input type="text" id="node-input-moduleName">
133
+ </div>
134
+ <div class="form-row">
135
+ <label for="node-input-channelCount"><i class="fa fa-list-ol"></i> Channels</label>
136
+ <input type="number" id="node-input-channelCount" min="1" max="32" style="width:80px">
137
+ <span style="margin-left:8px; color:#888; font-size:0.9em;">auto-filled from scan</span>
138
+ </div>
139
+ </script>
140
+
141
+ <script type="text/html" data-help-name="velbus-button">
142
+ <p style="background:#fff3cd; border-left:4px solid #ffc107; padding:8px 12px; margin-bottom:12px; font-size:0.85em;">
143
+ <strong>&#9888; Testing status:</strong> This node was generated with Claude.ai and is in
144
+ need of extensive field testing before being deployed into a commercial project.
145
+ It is presented "as is" &mdash; any use beyond testing is entirely at your own risk and liability.
146
+ Constructive feedback is welcomed, accompanied by as many examples and debug captures as possible.
147
+ <a href="https://github.com/MDAR/node-red-contrib-velbus-2026/issues" target="_blank">File an issue on GitHub</a>.
148
+ </p>
149
+ <p>
150
+ Button and input module node for Velbus push-button panels — original and V2 series.
151
+ For glass panel modules (VMBEL, VMBGP) with thermostat use <strong>velbus-glass-panel</strong> instead.
152
+ </p>
153
+
154
+ <h3>Supported modules</h3>
155
+ <p>VMB8PB, VMB8PBU, VMB6PBN, VMB4PB (original series) · VMB6PB-20 (V2 series).</p>
156
+ <p>Also suitable for glass panel sub-addresses when button events need separate wiring.</p>
157
+
158
+ <h3>Output</h3>
159
+ <pre>{
160
+ "type": "button",
161
+ "on": true,
162
+ "pressed": [1, 3],
163
+ "released": [],
164
+ "longPressed": []
165
+ }</pre>
166
+ <p>
167
+ <code>on</code> is <code>true</code> when any channel is pressed or long-pressed,
168
+ <code>false</code> when only releases are present.
169
+ Channel numbers are 1-indexed. All three arrays are always present —
170
+ empty arrays when no channels in that state.
171
+ </p>
172
+ </script>
@@ -0,0 +1,117 @@
1
+ 'use strict';
2
+
3
+ const { parsePkt } = require('../../lib/velbus-utils');
4
+
5
+ // ─────────────────────────────────────────────────────────────────────────────
6
+ // Button module type registry
7
+ // ─────────────────────────────────────────────────────────────────────────────
8
+ const BUTTON_TYPES = {
9
+ 0x01: { name: 'VMB8PB', channels: 8, series: 'original' },
10
+ 0x16: { name: 'VMB8PBU', channels: 8, series: 'original' },
11
+ 0x17: { name: 'VMB6PBN', channels: 6, series: 'original' },
12
+ 0x1C: { name: 'VMB4PB', channels: 4, series: 'original' },
13
+ 0x20: { name: 'VMB6PB-20', channels: 6, series: 'v2' },
14
+ };
15
+
16
+ const BUTTON_TYPE_IDS = new Set(Object.keys(BUTTON_TYPES).map(k => parseInt(k)));
17
+
18
+ // ─────────────────────────────────────────────────────────────────────────────
19
+ // Channel bitmask helper
20
+ // ─────────────────────────────────────────────────────────────────────────────
21
+ function bitsToChannels(byte1, byte2, byte3, byte4) {
22
+ const channels = [];
23
+ const bytes = [byte1 || 0, byte2 || 0, byte3 || 0, byte4 || 0];
24
+ for (let b = 0; b < 4; b++) {
25
+ for (let bit = 0; bit < 8; bit++) {
26
+ if (bytes[b] & (1 << bit)) channels.push(b * 8 + bit + 1);
27
+ }
28
+ }
29
+ return channels;
30
+ }
31
+
32
+ // ─────────────────────────────────────────────────────────────────────────────
33
+ // Node
34
+ // ─────────────────────────────────────────────────────────────────────────────
35
+ module.exports = function(RED) {
36
+
37
+ function VelbusButtonNode(config) {
38
+ RED.nodes.createNode(this, config);
39
+ const node = this;
40
+
41
+ node.bridge = RED.nodes.getNode(config.bridge);
42
+ node.address = typeof config.address === 'number'
43
+ ? config.address // legacy saves stored decimal numbers
44
+ : (parseInt(config.address, 16) || 0); // editor stores hex strings
45
+ node.channelCount = parseInt(config.channelCount) || 8;
46
+ node.moduleName = config.moduleName || '';
47
+ node.typeId = config.typeId ? parseInt(config.typeId) : null;
48
+
49
+ if (!node.bridge) {
50
+ node.status({ fill: 'red', shape: 'ring', text: 'no bridge' });
51
+ node.error('velbus-button: no bridge configured');
52
+ return;
53
+ }
54
+ if (!node.address || node.address < 1 || node.address > 254) {
55
+ node.status({ fill: 'red', shape: 'ring', text: 'invalid address' });
56
+ node.error('velbus-button: invalid address ' + node.address);
57
+ return;
58
+ }
59
+
60
+ const _label = node.moduleName
61
+ ? node.moduleName + ' (0x' + node.address.toString(16).padStart(2, '0').toUpperCase() + ')'
62
+ : '0x' + node.address.toString(16).padStart(2, '0').toUpperCase();
63
+
64
+ function setStatus(text, fill, shape) {
65
+ node.status({ fill: fill || 'green', shape: shape || 'dot', text: _label + ' ' + text });
66
+ }
67
+
68
+ // ── Packet handler ───────────────────────────────────────────────────
69
+
70
+ function onPacket(raw) {
71
+ const p = parsePkt(raw);
72
+ if (!p || p.rtr) return;
73
+ if (p.addr !== node.address) return;
74
+
75
+ const { cmd, body } = p;
76
+
77
+ // 0x00 — button press / release / long-press
78
+ if (cmd === 0x00) {
79
+ if (body.length < 2) return;
80
+
81
+ const pressed = bitsToChannels(body[0]);
82
+ const released = bitsToChannels(body[1]);
83
+ const longPressed = body.length >= 3 ? bitsToChannels(body[2]) : [];
84
+
85
+ const on = pressed.length > 0 || longPressed.length > 0;
86
+
87
+ const payload = {
88
+ type: 'button',
89
+ on,
90
+ pressed,
91
+ released,
92
+ longPressed,
93
+ };
94
+
95
+ setStatus(
96
+ pressed.length ? 'pressed ch' + pressed.join(',')
97
+ : longPressed.length ? 'long ch' + longPressed.join(',')
98
+ : 'released ch' + released.join(',')
99
+ );
100
+
101
+ node.send({ payload });
102
+ }
103
+ }
104
+
105
+ node.bridge.register(node.address, onPacket);
106
+
107
+ // ── Cleanup ──────────────────────────────────────────────────────────
108
+
109
+ node.on('close', function() {
110
+ node.bridge.deregister(node.address, onPacket);
111
+ });
112
+
113
+ setStatus('ready', 'grey', 'dot');
114
+ }
115
+
116
+ RED.nodes.registerType('velbus-button', VelbusButtonNode);
117
+ };
@@ -0,0 +1,198 @@
1
+ <script type="text/javascript">
2
+ (function() {
3
+
4
+ RED.nodes.registerType('velbus-clock', {
5
+ category: 'Velbus (outputs)',
6
+ color: '#4A90D9',
7
+ defaults: {
8
+ name: { value: '' },
9
+ bridge: { value: '', type: 'velbus-bridge', required: true },
10
+ autoBroadcast: { value: false },
11
+ intervalMinutes: { value: 60, validate: function(v) { return parseInt(v) > 0; } },
12
+ includeDate: { value: true },
13
+ includeDst: { value: true },
14
+ },
15
+ inputs: 1,
16
+ outputs: 1,
17
+ outputLabels: ['broadcast sent'],
18
+ icon: 'font-awesome/fa-clock-o',
19
+ paletteLabel: 'clock',
20
+
21
+ label: function() {
22
+ return this.name || (this.autoBroadcast
23
+ ? 'clock (auto, ' + this.intervalMinutes + 'm)'
24
+ : 'clock (manual)');
25
+ },
26
+
27
+ oneditprepare: function() {
28
+ function toggleIntervalRow() {
29
+ $('#clock-interval-row').toggle($('#node-input-autoBroadcast').is(':checked'));
30
+ }
31
+ $('#node-input-autoBroadcast').on('change', toggleIntervalRow);
32
+ toggleIntervalRow();
33
+ },
34
+ });
35
+
36
+ })();
37
+ </script>
38
+
39
+ <script type="text/html" data-template-name="velbus-clock">
40
+ <div class="form-row">
41
+ <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
42
+ <input type="text" id="node-input-name" placeholder="Optional label">
43
+ </div>
44
+ <div class="form-row">
45
+ <label for="node-input-bridge"><i class="fa fa-plug"></i> Bridge</label>
46
+ <input type="text" id="node-input-bridge">
47
+ </div>
48
+ <div class="form-row">
49
+ <label for="node-input-autoBroadcast"><i class="fa fa-refresh"></i> Auto-broadcast</label>
50
+ <input type="checkbox" id="node-input-autoBroadcast" style="width:auto; vertical-align:middle;">
51
+ <span style="color:#888; font-size:0.9em;">send automatically on an interval</span>
52
+ </div>
53
+ <div class="form-row" id="clock-interval-row">
54
+ <label for="node-input-intervalMinutes"><i class="fa fa-hourglass-half"></i> Interval (min)</label>
55
+ <input type="number" id="node-input-intervalMinutes" min="1" style="width:80px">
56
+ </div>
57
+ <div class="form-row">
58
+ <label for="node-input-includeDate"><i class="fa fa-calendar"></i> Include date</label>
59
+ <input type="checkbox" id="node-input-includeDate" style="width:auto; vertical-align:middle;">
60
+ <span style="color:#888; font-size:0.9em;">also send 0xB7 (day/month/year)</span>
61
+ </div>
62
+ <div class="form-row">
63
+ <label for="node-input-includeDst"><i class="fa fa-sun-o"></i> Include DST flag</label>
64
+ <input type="checkbox" id="node-input-includeDst" style="width:auto; vertical-align:middle;">
65
+ <span style="color:#888; font-size:0.9em;">also send 0xAF (daylight saving on/off)</span>
66
+ </div>
67
+ <div class="form-tips" style="margin-top:8px">
68
+ Broadcasts to bus address <code>0x00</code> — every RTC-equipped module on
69
+ the bus receives this and updates its own clock. Uses the Node-RED host's
70
+ own system clock and timezone, so make sure that's set correctly
71
+ (e.g. <code>timedatectl set-timezone Europe/London</code>).
72
+ </div>
73
+ </script>
74
+
75
+ <script type="text/html" data-help-name="velbus-clock">
76
+ <p style="background:#fff3cd; border-left:4px solid #ffc107; padding:8px 12px; margin-bottom:12px; font-size:0.85em;">
77
+ <strong>&#9888; Testing status:</strong> This node was generated with Claude.ai and is in
78
+ need of extensive field testing before being deployed into a commercial project.
79
+ It is presented "as is" &mdash; any use beyond testing is entirely at your own risk and liability.
80
+ Constructive feedback is welcomed, accompanied by as many examples and debug captures as possible.
81
+ <a href="https://github.com/MDAR/node-red-contrib-velbus-2026/issues" target="_blank">File an issue on GitHub</a>.
82
+ </p>
83
+ <p>
84
+ Broadcasts system time, date, and daylight-saving state to the bus broadcast
85
+ address <code>0x00</code>. Every RTC-equipped Velbus module (relays, dimmers,
86
+ glass panels with clock features, etc.) receives and updates its own real-time
87
+ clock from this — this is what a physical Signum master clock module normally
88
+ does. Use this node if you don't have one fitted, or want Node-RED to be the
89
+ time source instead.
90
+ </p>
91
+
92
+ <h3>Commands sent</h3>
93
+ <ul>
94
+ <li><code>0xD8</code> Set real time clock — day of week, hour, minute</li>
95
+ <li><code>0xB7</code> Set date — day, month, year (only if "Include date" is on)</li>
96
+ <li><code>0xAF</code> Set daylight savings — on/off flag (only if "Include DST flag" is on)</li>
97
+ </ul>
98
+ <p>
99
+ All three are sent to address <code>0x00</code> at low priority, matching every
100
+ module protocol PDF checked. <b>Never</b> configure <code>0x00</code> as a real
101
+ module's own address — it is reserved for bus-wide broadcast.
102
+ </p>
103
+
104
+ <h3>DST detection</h3>
105
+ <p>
106
+ The daylight-saving flag is worked out from the Node-RED host's own configured
107
+ timezone (comparing the current UTC offset against the year's Jan 1 / Jul 1
108
+ offsets) — correct for UK/EU/US-style single-transition DST, but only as
109
+ accurate as the host's own clock and <code>TZ</code> setting. Override it
110
+ manually with <code>msg.payload.dst</code> (see below) if needed.
111
+ </p>
112
+
113
+ <h3>Input — Commands</h3>
114
+ <pre>
115
+ {} // bare inject — broadcast time (+date/DST per config), using now
116
+ { "cmd": "broadcast" }
117
+ { "cmd": "broadcast", "date": "2026-07-06T14:32:00" } // override the time sent (testing)
118
+ { "cmd": "broadcast", "dst": true } // override the DST flag sent
119
+ { "cmd": "broadcast_time" } // 0xD8 only, ignores config toggles
120
+ { "cmd": "broadcast_date" } // 0xB7 only
121
+ { "cmd": "broadcast_dst" } // 0xAF only, auto-detected
122
+ { "cmd": "broadcast_dst", "dst": false } // 0xAF only, forced value
123
+ { "cmd": "set_alarm", "alarm": 1, "wakeHour": 7, "wakeMinute": 15, "bedHour": 22, "bedMinute": 30 }
124
+ { "cmd": "set_alarm", "alarm": 2, "wakeHour": 6, "wakeMinute": 45, "bedHour": 23, "bedMinute": 0, "address": "0x30" }
125
+ { "cmd": "set_alarm", "alarm": 1, "enabled": false } // disable, address omitted = global
126
+ </pre>
127
+
128
+ <h3>Output</h3>
129
+ <pre>
130
+ {
131
+ "topic": "clock_broadcast",
132
+ "dayOfWeek": 1, // 0=Monday...6=Sunday (Velbus convention, not JS)
133
+ "hours": 14,
134
+ "minutes": 32,
135
+ "date": { "day": 6, "month": 7, "year": 2026 }, // null if date not included
136
+ "dst": false, // null if DST flag not included
137
+ "timestamp": 1783348320000
138
+ }
139
+ </pre>
140
+ <p>
141
+ Only emitted for <code>"cmd": "broadcast"</code> / bare inject. The single-purpose
142
+ commands (<code>broadcast_time</code> etc.) update the status text but don't emit
143
+ an output message.
144
+ </p>
145
+
146
+ <h3>Auto-broadcast</h3>
147
+ <p>
148
+ When enabled, sends automatically on the configured interval (first send
149
+ 5 seconds after deploy, to give the bridge time to connect). Manual input
150
+ commands still work alongside this.
151
+ </p>
152
+
153
+ <h3>set_alarm — global or local clock alarm (0xC3)</h3>
154
+ <p>
155
+ Sets a wake/sleep alarm (alarm 1 or 2). Omit <code>address</code> for a
156
+ <b>global</b> alarm — every V2 module on the bus receives the same
157
+ wake/sleep schedule. Provide <code>address</code> (hex string or decimal)
158
+ for a <b>local</b> alarm on that one module only. The packet body is
159
+ identical either way — only the destination address differs, per the
160
+ protocol's own design.
161
+ </p>
162
+ <p>
163
+ <b>Note on scope:</b> this is the one command on this node that breaks
164
+ from the palette's usual "one node = one physical module" pattern — it
165
+ accepts a per-message address override instead of a fixed node config.
166
+ That's deliberate: 0xC3 is a shared V2 system command any module type can
167
+ receive (confirmed identical across relay-20, dimmer-20, and glass-panel-20
168
+ protocol PDFs), not a feature specific to one module type the way an RGBW
169
+ setting or thermostat setpoint is — so it lives here rather than being
170
+ duplicated across every V2 node type.
171
+ </p>
172
+ <pre>
173
+ { "cmd": "set_alarm", "alarm": 1, "wakeHour": 7, "wakeMinute": 15, "bedHour": 22, "bedMinute": 30 }
174
+ // → global: every module gets this schedule
175
+
176
+ { "cmd": "set_alarm", "alarm": 2, "wakeHour": 6, "wakeMinute": 45, "bedHour": 23, "bedMinute": 0, "address": "0x30" }
177
+ // → local: only module 0x30 gets this schedule
178
+
179
+ { "cmd": "set_alarm", "alarm": 1, "enabled": false }
180
+ // → disable alarm 1 (global) — wakeHour/wakeMinute/bedHour/bedMinute default to 0 if omitted
181
+ </pre>
182
+ <p>Output:</p>
183
+ <pre>
184
+ {
185
+ "topic": "alarm_set",
186
+ "target": "global", // or "0x30" etc. for a local alarm
187
+ "alarm": 1,
188
+ "wakeHour": 7,
189
+ "wakeMinute": 15,
190
+ "bedHour": 22,
191
+ "bedMinute": 30,
192
+ "enabled": true
193
+ }
194
+ </pre>
195
+ <p><code>enabled</code> defaults to <code>true</code> if omitted. An invalid
196
+ <code>address</code> falls back to a global broadcast with a warning, rather
197
+ than silently failing.</p>
198
+ </script>