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.
- package/CHANGELOG_FORUM.md +784 -0
- package/LICENSE +21 -0
- package/README.md +140 -0
- package/examples/velbus-basic-relay-dimmer.json +328 -0
- package/index.js +3 -0
- package/lib/blind-types-20.js +26 -0
- package/lib/blind-types-s.js +46 -0
- package/lib/blind-types.js +38 -0
- package/lib/dimmer-types-20.js +120 -0
- package/lib/dimmer-types.js +34 -0
- package/lib/glass-panel-types.js +296 -0
- package/lib/pir-types-20.js +51 -0
- package/lib/pir-types.js +61 -0
- package/lib/relay-types-20.js +43 -0
- package/lib/relay-types.js +39 -0
- package/lib/sensor-types-20.js +29 -0
- package/lib/sensor-types.js +49 -0
- package/lib/velbus-utils.js +80 -0
- package/nodes/velbus-blind/velbus-blind.html +165 -0
- package/nodes/velbus-blind/velbus-blind.js +251 -0
- package/nodes/velbus-blind-20/velbus-blind-20.html +179 -0
- package/nodes/velbus-blind-20/velbus-blind-20.js +300 -0
- package/nodes/velbus-blind-s/velbus-blind-s.html +181 -0
- package/nodes/velbus-blind-s/velbus-blind-s.js +282 -0
- package/nodes/velbus-bridge/velbus-bridge.html +72 -0
- package/nodes/velbus-bridge/velbus-bridge.js +304 -0
- package/nodes/velbus-button/velbus-button.html +172 -0
- package/nodes/velbus-button/velbus-button.js +117 -0
- package/nodes/velbus-clock/velbus-clock.html +198 -0
- package/nodes/velbus-clock/velbus-clock.js +273 -0
- package/nodes/velbus-dimmer/velbus-dimmer.html +174 -0
- package/nodes/velbus-dimmer/velbus-dimmer.js +457 -0
- package/nodes/velbus-dimmer-20/velbus-dimmer-20.html +271 -0
- package/nodes/velbus-dimmer-20/velbus-dimmer-20.js +602 -0
- package/nodes/velbus-glass-panel/velbus-glass-panel.html +337 -0
- package/nodes/velbus-glass-panel/velbus-glass-panel.js +492 -0
- package/nodes/velbus-meteo/velbus-meteo.html +120 -0
- package/nodes/velbus-meteo/velbus-meteo.js +279 -0
- package/nodes/velbus-pir/velbus-pir.html +187 -0
- package/nodes/velbus-pir/velbus-pir.js +269 -0
- package/nodes/velbus-pir-20/velbus-pir-20.html +186 -0
- package/nodes/velbus-pir-20/velbus-pir-20.js +352 -0
- package/nodes/velbus-relay/velbus-relay.html +215 -0
- package/nodes/velbus-relay/velbus-relay.js +404 -0
- package/nodes/velbus-relay-20/velbus-relay-20.html +123 -0
- package/nodes/velbus-relay-20/velbus-relay-20.js +434 -0
- package/nodes/velbus-scan/velbus-scan.html +87 -0
- package/nodes/velbus-scan/velbus-scan.js +313 -0
- package/nodes/velbus-sensor/velbus-sensor.html +154 -0
- package/nodes/velbus-sensor/velbus-sensor.js +259 -0
- package/nodes/velbus-sensor-20/velbus-sensor-20.html +158 -0
- package/nodes/velbus-sensor-20/velbus-sensor-20.js +314 -0
- package/nodes/velbus-thermostat/velbus-thermostat.html +191 -0
- package/nodes/velbus-thermostat/velbus-thermostat.js +322 -0
- package/package.json +55 -0
- 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>⚠ 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" — 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>⚠ 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" — 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>
|