node-red-contrib-knx-ultimate 4.0.13 → 4.0.15
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.md +10 -0
- package/nodes/commonFunctions.js +150 -0
- package/nodes/knxUltimate-config.js +1 -1
- package/nodes/knxUltimateGarage.html +2 -2
- package/nodes/knxUltimateIoTBridge.html +387 -0
- package/nodes/knxUltimateIoTBridge.js +500 -0
- package/nodes/knxUltimateStaircase.html +2 -2
- package/nodes/locales/de/knxUltimateIoTBridge.html +150 -0
- package/nodes/locales/de/knxUltimateIoTBridge.json +77 -0
- package/nodes/locales/en/knxUltimateIoTBridge.html +150 -0
- package/nodes/locales/en/knxUltimateIoTBridge.json +77 -0
- package/nodes/locales/es/knxUltimateIoTBridge.html +150 -0
- package/nodes/locales/es/knxUltimateIoTBridge.json +77 -0
- package/nodes/locales/fr/knxUltimateIoTBridge.html +150 -0
- package/nodes/locales/fr/knxUltimateIoTBridge.json +77 -0
- package/nodes/locales/it/knxUltimateIoTBridge.html +150 -0
- package/nodes/locales/it/knxUltimateIoTBridge.json +77 -0
- package/nodes/locales/zh-CN/knxUltimateIoTBridge.html +150 -0
- package/nodes/locales/zh-CN/knxUltimateIoTBridge.json +77 -0
- package/nodes/plugins/knxUltimateMonitor-sidebar-plugin.html +542 -0
- package/package.json +8 -6
- package/scripts/check-node-docs.js +82 -0
- package/scripts/manage-wiki-menu.js +34 -22
- package/scripts/migrate-node-help.js +6 -5
- package/scripts/translate-wiki.js +18 -7
- package/scripts/update-help-from-wiki.js +3 -4
- package/scripts/validate-wiki-languagebar.js +30 -18
- package/scripts/wiki-menu.json +10 -0
- package/tutorial/knxUltimate-AllNodes-Presentazione.md +146 -151
- package/tutorial/knxUltimateFutureNodes.md +11 -0
package/CHANGELOG.md
CHANGED
|
@@ -6,7 +6,17 @@
|
|
|
6
6
|
|
|
7
7
|
# CHANGELOG
|
|
8
8
|
|
|
9
|
+
**Version 4.0.15** - October 2025<br/>
|
|
10
|
+
- NEW: Introduced the KNX Monitor sidebar plugin with live GA table, 1 s auto-refresh, highlight on fresh telegrams and row toggles for boolean values.<br/>
|
|
11
|
+
- NEW: Added inline filtering, draggable column widths, persistent gateway selection and reorder controls to speed up commissioning and diagnosis.<br/>
|
|
12
|
+
- Added `/knxUltimateMonitor` and `/knxUltimateMonitorToggle` admin endpoints to feed the sidebar and allow direct write/toggle actions.<br/>
|
|
13
|
+
- Docs: documented the KNX Monitor panel across all supported wiki languages and refreshed screenshots/tooltips.<br/>
|
|
14
|
+
|
|
15
|
+
**Version 4.0.14** - October 2025<br/>
|
|
16
|
+
- NEW: Added KNX ↔ IoT Bridge node to orchestrate bidirectional KNX↔MQTT/REST/Modbus mappings with scaling, templating and ack metadata.<br/>
|
|
17
|
+
|
|
9
18
|
**Version 4.0.13** - October 2025<br/>
|
|
19
|
+
- NEW: Added KNX ↔ IoT Bridge node to orchestrate bidirectional KNX↔MQTT/REST/Modbus mappings with scaling, templating and ack metadata.<br/>
|
|
10
20
|
- NEW: Added KNX Garage Door node with boolean/impulse control, hold-open and disable GAs, safety integration and automatic re-close timer.<br/>
|
|
11
21
|
- KNX Staircase node: editor refinements with read-only DPTs, flow payload support and localisation polish across supported languages.<br/>
|
|
12
22
|
- KNX Staircase & Garage nodes: unified status handling and multilingual flow examples in docs/help.<br/>
|
package/nodes/commonFunctions.js
CHANGED
|
@@ -61,6 +61,52 @@ module.exports = (RED) => {
|
|
|
61
61
|
function commonFunctions() {
|
|
62
62
|
var node = this;
|
|
63
63
|
|
|
64
|
+
const ensureBuffer = (rawValue) => {
|
|
65
|
+
if (!rawValue) return null;
|
|
66
|
+
if (Buffer.isBuffer(rawValue)) return rawValue;
|
|
67
|
+
if (Array.isArray(rawValue)) return Buffer.from(rawValue);
|
|
68
|
+
if (typeof rawValue === 'object' && Array.isArray(rawValue.data)) return Buffer.from(rawValue.data);
|
|
69
|
+
if (rawValue.type === 'Buffer' && Array.isArray(rawValue.data)) return Buffer.from(rawValue.data);
|
|
70
|
+
return null;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const guessDptFromRawValue = (rawBuffer) => {
|
|
74
|
+
if (!rawBuffer || !rawBuffer.length) return null;
|
|
75
|
+
if (rawBuffer.length === 1) {
|
|
76
|
+
if (rawBuffer[0] === 0 || rawBuffer[0] === 1) return '1.001';
|
|
77
|
+
return '5.001';
|
|
78
|
+
}
|
|
79
|
+
if (rawBuffer.length === 4) return '14.056';
|
|
80
|
+
if (rawBuffer.length === 2) return '9.001';
|
|
81
|
+
if (rawBuffer.length === 3) return '11.001';
|
|
82
|
+
if (rawBuffer.length === 14) return '16.001';
|
|
83
|
+
|
|
84
|
+
const dpts = Object.entries(dptlib).filter(onlyDptKeys).map(extractBaseNo).sort(sortBy('base')).reduce(toConcattedSubtypes, []);
|
|
85
|
+
for (let index = 0; index < dpts.length; index++) {
|
|
86
|
+
try {
|
|
87
|
+
const resolved = dptlib.resolve(dpts[index].value);
|
|
88
|
+
if (!resolved) continue;
|
|
89
|
+
const jsValue = dptlib.fromBuffer(rawBuffer, resolved);
|
|
90
|
+
if (typeof jsValue !== 'undefined') return dpts[index].value;
|
|
91
|
+
} catch (error) { /* empty */ }
|
|
92
|
+
}
|
|
93
|
+
return null;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const formatDisplayValue = (value) => {
|
|
97
|
+
if (value === null || value === undefined) return '';
|
|
98
|
+
if (value instanceof Date) return value.toISOString();
|
|
99
|
+
if (typeof value === 'object') {
|
|
100
|
+
try {
|
|
101
|
+
return JSON.stringify(value);
|
|
102
|
+
} catch (error) {
|
|
103
|
+
return String(value);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (typeof value === 'boolean') return value ? 'true' : 'false';
|
|
107
|
+
return String(value);
|
|
108
|
+
};
|
|
109
|
+
|
|
64
110
|
// // Gather infos about all interfaces on the lan and provides a static variable utils.aDiscoveredknxGateways
|
|
65
111
|
// try {
|
|
66
112
|
// require('./utils/utils').DiscoverKNXGateways()
|
|
@@ -499,6 +545,110 @@ module.exports = (RED) => {
|
|
|
499
545
|
}
|
|
500
546
|
});
|
|
501
547
|
|
|
548
|
+
RED.httpAdmin.get('/knxUltimateMonitor', RED.auth.needsPermission("knxUltimate-config.read"), (req, res) => {
|
|
549
|
+
try {
|
|
550
|
+
const server = RED.nodes.getNode(req.query.serverId);
|
|
551
|
+
if (!server) {
|
|
552
|
+
res.json({ items: [], error: 'NO_SERVER' });
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
const items = Array.isArray(server.exposedGAs) ? server.exposedGAs.map((entry) => {
|
|
556
|
+
const rawBuffer = ensureBuffer(entry?.rawValue);
|
|
557
|
+
const rawHex = rawBuffer ? rawBuffer.toString('hex') : '';
|
|
558
|
+
let dpt = entry?.dpt;
|
|
559
|
+
let guessed = false;
|
|
560
|
+
if ((!dpt || dpt === '') && rawBuffer) {
|
|
561
|
+
const inferred = guessDptFromRawValue(rawBuffer);
|
|
562
|
+
if (inferred) {
|
|
563
|
+
dpt = inferred;
|
|
564
|
+
guessed = true;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
let value = null;
|
|
568
|
+
if (rawBuffer && dpt) {
|
|
569
|
+
try {
|
|
570
|
+
const resolved = dptlib.resolve(dpt);
|
|
571
|
+
if (resolved) value = dptlib.fromBuffer(rawBuffer, resolved);
|
|
572
|
+
} catch (error) { /* empty */ }
|
|
573
|
+
}
|
|
574
|
+
return {
|
|
575
|
+
ga: entry?.ga || '',
|
|
576
|
+
devicename: entry?.devicename || '',
|
|
577
|
+
dpt: dpt || '',
|
|
578
|
+
dptGuessed: guessed,
|
|
579
|
+
rawHex,
|
|
580
|
+
value,
|
|
581
|
+
valueText: formatDisplayValue(value),
|
|
582
|
+
updatedAt: entry?.updatedAt || null,
|
|
583
|
+
};
|
|
584
|
+
}).sort((a, b) => a.ga.localeCompare(b.ga)) : [];
|
|
585
|
+
res.json({ items });
|
|
586
|
+
} catch (error) {
|
|
587
|
+
try { RED.log.error(`KNXUltimate: knxUltimateMonitor error: ${error.message}`); } catch (e) { }
|
|
588
|
+
res.json({ items: [], error: error.message });
|
|
589
|
+
}
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
RED.httpAdmin.post('/knxUltimateMonitorToggle', RED.auth.needsPermission("knxUltimate-config.write"), (req, res) => {
|
|
593
|
+
try {
|
|
594
|
+
const serverId = req.body?.serverId;
|
|
595
|
+
const ga = req.body?.ga;
|
|
596
|
+
const server = serverId ? RED.nodes.getNode(serverId) : null;
|
|
597
|
+
if (!server) {
|
|
598
|
+
res.json({ status: 'error', error: 'NO_SERVER' });
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
if (!ga) {
|
|
602
|
+
res.json({ status: 'error', error: 'NO_GA' });
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
const entry = Array.isArray(server.exposedGAs) ? server.exposedGAs.find((item) => item.ga === ga) : undefined;
|
|
606
|
+
if (!entry) {
|
|
607
|
+
res.json({ status: 'error', error: 'GA_NOT_FOUND' });
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
const rawBuffer = ensureBuffer(entry.rawValue);
|
|
611
|
+
let dpt = entry.dpt;
|
|
612
|
+
if (!rawBuffer) {
|
|
613
|
+
res.json({ status: 'error', error: 'NO_CURRENT_VALUE' });
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
if (!dpt) {
|
|
617
|
+
const inferred = guessDptFromRawValue(rawBuffer);
|
|
618
|
+
if (inferred) dpt = inferred;
|
|
619
|
+
}
|
|
620
|
+
let currentValue = null;
|
|
621
|
+
if (dpt) {
|
|
622
|
+
try {
|
|
623
|
+
const resolved = dptlib.resolve(dpt);
|
|
624
|
+
if (resolved) currentValue = dptlib.fromBuffer(rawBuffer, resolved);
|
|
625
|
+
} catch (error) { currentValue = null; }
|
|
626
|
+
}
|
|
627
|
+
if (typeof currentValue !== 'boolean') {
|
|
628
|
+
res.json({ status: 'error', error: 'NOT_BOOLEAN' });
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
const nextValue = !currentValue;
|
|
632
|
+
const sendDpt = dpt || '1.001';
|
|
633
|
+
try {
|
|
634
|
+
server.sendKNXTelegramToKNXEngine({
|
|
635
|
+
grpaddr: ga,
|
|
636
|
+
payload: nextValue,
|
|
637
|
+
dpt: sendDpt,
|
|
638
|
+
outputtype: 'write',
|
|
639
|
+
nodecallerid: 'knxUltimateMonitor'
|
|
640
|
+
});
|
|
641
|
+
} catch (error) {
|
|
642
|
+
res.json({ status: 'error', error: error.message });
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
res.json({ status: 'ok', value: nextValue });
|
|
646
|
+
} catch (error) {
|
|
647
|
+
try { RED.log.error(`KNXUltimate: knxUltimateMonitorToggle error: ${error.message}`); } catch (e) { }
|
|
648
|
+
res.json({ status: 'error', error: error.message });
|
|
649
|
+
}
|
|
650
|
+
});
|
|
651
|
+
|
|
502
652
|
RED.httpAdmin.get("/KNXUltimateGetResourcesHUE", (req, res) => {
|
|
503
653
|
try {
|
|
504
654
|
// °°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°
|
|
@@ -1154,7 +1154,7 @@ module.exports = (RED) => {
|
|
|
1154
1154
|
// 04/04/2021 Supergiovane: save value to node.exposedGAs
|
|
1155
1155
|
if (typeof _dest === "string" && _rawValue !== undefined && (_evt === "GroupValue_Write" || _evt === "GroupValue_Response")) {
|
|
1156
1156
|
try {
|
|
1157
|
-
const ret = { ga: _dest, rawValue: _rawValue, dpt: undefined, devicename: undefined };
|
|
1157
|
+
const ret = { ga: _dest, rawValue: _rawValue, dpt: undefined, devicename: undefined, updatedAt: Date.now() };
|
|
1158
1158
|
node.exposedGAs = node.exposedGAs.filter((item) => item.ga !== _dest); // Remove previous
|
|
1159
1159
|
if (node.csv !== undefined && node.csv !== '' && node.csv.length !== 0) {
|
|
1160
1160
|
// Add the dpt
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
<script type="text/javascript">
|
|
4
4
|
RED.nodes.registerType('knxUltimateGarage', {
|
|
5
5
|
category: 'KNX Ultimate',
|
|
6
|
-
color: '#
|
|
6
|
+
color: '#C7E9C0',
|
|
7
7
|
defaults: {
|
|
8
8
|
server: { type: 'knxUltimate-config', required: true },
|
|
9
9
|
name: { value: '' },
|
|
@@ -306,4 +306,4 @@
|
|
|
306
306
|
</ul>
|
|
307
307
|
<h3 data-i18n="knxUltimateGarage.help.events"></h3>
|
|
308
308
|
<p data-i18n="knxUltimateGarage.help.events_desc"></p>
|
|
309
|
-
</script>
|
|
309
|
+
</script>
|
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
<script type="text/javascript" src="resources/node-red-contrib-knx-ultimate/htmlUtils.js"></script>
|
|
2
|
+
|
|
3
|
+
<script type="text/javascript">
|
|
4
|
+
RED.nodes.registerType('knxUltimateIoTBridge', {
|
|
5
|
+
category: 'KNX Ultimate',
|
|
6
|
+
color: '#C7E9C0',
|
|
7
|
+
defaults: {
|
|
8
|
+
server: { type: 'knxUltimate-config', required: false },
|
|
9
|
+
name: { value: '' },
|
|
10
|
+
outputtopic: { value: '' },
|
|
11
|
+
emitOnChangeOnly: { value: true },
|
|
12
|
+
readOnDeploy: { value: true },
|
|
13
|
+
acceptFlowInput: { value: true },
|
|
14
|
+
mappings: { value: [] }
|
|
15
|
+
},
|
|
16
|
+
inputs: 1,
|
|
17
|
+
outputs: 2,
|
|
18
|
+
outputLabels: function (index) {
|
|
19
|
+
if (index === 0) return this._('knxUltimateIoTBridge.labels.outputKnxToIoT');
|
|
20
|
+
if (index === 1) return this._('knxUltimateIoTBridge.labels.outputIoTToKnx');
|
|
21
|
+
},
|
|
22
|
+
icon: 'node-knx-icon.svg',
|
|
23
|
+
paletteLabel: function () {
|
|
24
|
+
return this._('knxUltimateIoTBridge.paletteLabel');
|
|
25
|
+
},
|
|
26
|
+
label: function () {
|
|
27
|
+
return this.name || this._('knxUltimateIoTBridge.title');
|
|
28
|
+
},
|
|
29
|
+
oneditprepare: function () {
|
|
30
|
+
try {
|
|
31
|
+
RED.sidebar.show('help');
|
|
32
|
+
} catch (error) { }
|
|
33
|
+
|
|
34
|
+
const node = this;
|
|
35
|
+
let oNodeServer = RED.nodes.node($('#node-input-server').val());
|
|
36
|
+
|
|
37
|
+
$('#node-input-server').change(function () {
|
|
38
|
+
try {
|
|
39
|
+
oNodeServer = RED.nodes.node($(this).val());
|
|
40
|
+
} catch (error) { }
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const directions = [
|
|
44
|
+
{ value: 'bidirectional', label: node._('knxUltimateIoTBridge.direction.bidirectional') },
|
|
45
|
+
{ value: 'knx-to-iot', label: node._('knxUltimateIoTBridge.direction.knx-to-iot') },
|
|
46
|
+
{ value: 'iot-to-knx', label: node._('knxUltimateIoTBridge.direction.iot-to-knx') }
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
const channelTypes = [
|
|
50
|
+
{ value: 'mqtt', label: node._('knxUltimateIoTBridge.type.mqtt') },
|
|
51
|
+
{ value: 'rest', label: node._('knxUltimateIoTBridge.type.rest') },
|
|
52
|
+
{ value: 'modbus', label: node._('knxUltimateIoTBridge.type.modbus') }
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
function createLabeledField(row, key, element, options = {}) {
|
|
56
|
+
const wrapper = $('<div/>', {
|
|
57
|
+
class: 'bridge-field-wrapper',
|
|
58
|
+
style: 'display:flex; flex-direction:column; margin-right:10px;'
|
|
59
|
+
}).appendTo(row);
|
|
60
|
+
if (options.width) wrapper.css('width', options.width);
|
|
61
|
+
if (options.flex) wrapper.css('flex', options.flex);
|
|
62
|
+
const labelSpan = $('<span/>', {
|
|
63
|
+
class: 'bridge-field-label',
|
|
64
|
+
text: node._('knxUltimateIoTBridge.fields.' + key),
|
|
65
|
+
style: 'font-size:11px; color:#666; margin-bottom:2px;'
|
|
66
|
+
}).appendTo(wrapper);
|
|
67
|
+
element.css('width', '100%');
|
|
68
|
+
element.appendTo(wrapper);
|
|
69
|
+
element.data('bridgeWrapper', wrapper);
|
|
70
|
+
element.data('bridgeLabel', labelSpan);
|
|
71
|
+
return element;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const buildSelect = (options, selected) => {
|
|
75
|
+
const select = $('<select/>', { class: 'form-control input-small' });
|
|
76
|
+
options.forEach(opt => {
|
|
77
|
+
const option = $('<option/>', { value: opt.value }).text(opt.label);
|
|
78
|
+
if (opt.value === selected) option.attr('selected', 'selected');
|
|
79
|
+
select.append(option);
|
|
80
|
+
});
|
|
81
|
+
return select;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const container = $('#node-input-mapping-container');
|
|
85
|
+
container.css('min-height', '320px').css('min-width', '640px').editableList({
|
|
86
|
+
sortable: true,
|
|
87
|
+
removable: true,
|
|
88
|
+
addItem: function (row, index, data) {
|
|
89
|
+
const mapping = $.extend(true, {
|
|
90
|
+
id: '',
|
|
91
|
+
enabled: true,
|
|
92
|
+
label: '',
|
|
93
|
+
ga: '',
|
|
94
|
+
dpt: '',
|
|
95
|
+
direction: 'bidirectional',
|
|
96
|
+
iotType: 'mqtt',
|
|
97
|
+
target: '',
|
|
98
|
+
method: 'POST',
|
|
99
|
+
modbusFunction: 'writeHoldingRegister',
|
|
100
|
+
scale: 1,
|
|
101
|
+
offset: 0,
|
|
102
|
+
template: '',
|
|
103
|
+
property: '',
|
|
104
|
+
timeout: 0,
|
|
105
|
+
retry: 0
|
|
106
|
+
}, data.mapping);
|
|
107
|
+
|
|
108
|
+
if (!mapping.id || mapping.id === '') {
|
|
109
|
+
try {
|
|
110
|
+
mapping.id = RED.util.generateId();
|
|
111
|
+
} catch (error) {
|
|
112
|
+
mapping.id = Math.random().toString(16).slice(2);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const block = $('<div/>').addClass('knxultimate-bridge-block').appendTo(row);
|
|
117
|
+
|
|
118
|
+
const topRow = $('<div/>').addClass('form-row').css({ display: 'flex', alignItems: 'flex-end', flexWrap: 'wrap', gap: '12px' }).appendTo(block);
|
|
119
|
+
const midRow = $('<div/>').addClass('form-row').css({ display: 'flex', alignItems: 'flex-end', flexWrap: 'wrap', gap: '12px', marginTop: '8px' }).appendTo(block);
|
|
120
|
+
const bottomRow = $('<div/>').addClass('form-row').css({ display: 'flex', alignItems: 'flex-end', flexWrap: 'wrap', gap: '12px', marginTop: '8px' }).appendTo(block);
|
|
121
|
+
|
|
122
|
+
const checkboxWrapper = $('<label/>', { class: 'bridge-enabled-wrapper', style: 'display:flex; align-items:center; gap:6px; padding:4px 8px 4px 6px; border:1px solid #ced4da; border-radius:4px; background:#fff; cursor:pointer;' }).appendTo(topRow);
|
|
123
|
+
const enabled = $('<input/>', { type: 'checkbox', class: 'bridge-enabled', style: 'margin:0;' }).appendTo(checkboxWrapper);
|
|
124
|
+
$('<span/>', { text: node._('knxUltimateIoTBridge.mapping.enabled'), style: 'font-size:12px; font-weight:500;' }).appendTo(checkboxWrapper);
|
|
125
|
+
enabled.prop('checked', mapping.enabled !== false);
|
|
126
|
+
|
|
127
|
+
const labelInput = createLabeledField(topRow, 'label', $('<input/>', {
|
|
128
|
+
type: 'text',
|
|
129
|
+
class: 'form-control bridge-label'
|
|
130
|
+
}).attr('placeholder', node._('knxUltimateIoTBridge.placeholders.label')).val(mapping.label), { width: '190px' });
|
|
131
|
+
|
|
132
|
+
const gaInput = createLabeledField(topRow, 'ga', $('<input/>', {
|
|
133
|
+
type: 'text',
|
|
134
|
+
class: 'form-control bridge-ga'
|
|
135
|
+
}).attr('placeholder', node._('knxUltimateIoTBridge.placeholders.ga')).val(mapping.ga), { width: '140px' });
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
if (oNodeServer && oNodeServer.id) KNX_enableSecureFormatting(gaInput, oNodeServer.id);
|
|
139
|
+
} catch (error) { }
|
|
140
|
+
|
|
141
|
+
gaInput.autocomplete({
|
|
142
|
+
minLength: 0,
|
|
143
|
+
source: function (request, response) {
|
|
144
|
+
$.getJSON('knxUltimatecsv?nodeID=' + oNodeServer?.id, (data) => {
|
|
145
|
+
response($.map(data, (value) => {
|
|
146
|
+
const search = (value.ga + ' (' + value.devicename + ') DPT' + value.dpt);
|
|
147
|
+
if (htmlUtilsfullCSVSearch(search, request.term + ' 1.')) {
|
|
148
|
+
return {
|
|
149
|
+
label: value.ga + ' # ' + value.devicename + ' # ' + value.dpt,
|
|
150
|
+
value: value.ga,
|
|
151
|
+
dpt: value.dpt
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
return null;
|
|
155
|
+
}));
|
|
156
|
+
});
|
|
157
|
+
},
|
|
158
|
+
select: function (event, ui) {
|
|
159
|
+
if (!dptInput.val()) dptInput.val(ui.item.dpt);
|
|
160
|
+
if (!labelInput.val()) labelInput.val(ui.item.label.split('#')[1]?.trim() || '');
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
gaInput.on('focus.knxUltimateIoTBridge click.knxUltimateIoTBridge', function () {
|
|
165
|
+
try { $(this).autocomplete('search', ''); } catch (error) { }
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
const dptInput = createLabeledField(topRow, 'dpt', $('<input/>', {
|
|
169
|
+
type: 'text',
|
|
170
|
+
class: 'form-control bridge-dpt'
|
|
171
|
+
}).attr('placeholder', node._('knxUltimateIoTBridge.placeholders.dpt')).val(mapping.dpt), { width: '100px' });
|
|
172
|
+
|
|
173
|
+
const directionSelect = createLabeledField(topRow, 'direction', buildSelect(directions, mapping.direction).addClass('bridge-direction'), { width: '150px' });
|
|
174
|
+
|
|
175
|
+
const typeSelect = createLabeledField(topRow, 'channel', buildSelect(channelTypes, mapping.iotType).addClass('bridge-type'), { width: '140px' });
|
|
176
|
+
|
|
177
|
+
const targetInput = createLabeledField(midRow, 'target', $('<input/>', {
|
|
178
|
+
type: 'text',
|
|
179
|
+
class: 'form-control bridge-target'
|
|
180
|
+
}).attr('placeholder', node._('knxUltimateIoTBridge.placeholders.target')).val(mapping.target), { flex: '1 1 280px' });
|
|
181
|
+
|
|
182
|
+
const methodInput = createLabeledField(midRow, 'method', $('<input/>', {
|
|
183
|
+
type: 'text',
|
|
184
|
+
class: 'form-control bridge-method'
|
|
185
|
+
}).attr('placeholder', node._('knxUltimateIoTBridge.placeholders.method')).val(mapping.method), { width: '120px' });
|
|
186
|
+
|
|
187
|
+
const modbusInput = createLabeledField(midRow, 'modbusFunction', $('<input/>', {
|
|
188
|
+
type: 'text',
|
|
189
|
+
class: 'form-control bridge-modbus'
|
|
190
|
+
}).attr('placeholder', node._('knxUltimateIoTBridge.placeholders.modbusFunction')).val(mapping.modbusFunction), { width: '180px' });
|
|
191
|
+
|
|
192
|
+
const scaleInput = createLabeledField(midRow, 'scale', $('<input/>', {
|
|
193
|
+
type: 'number',
|
|
194
|
+
class: 'form-control bridge-scale'
|
|
195
|
+
}).attr('step', 'any').val(mapping.scale), { width: '100px' });
|
|
196
|
+
|
|
197
|
+
const offsetInput = createLabeledField(midRow, 'offset', $('<input/>', {
|
|
198
|
+
type: 'number',
|
|
199
|
+
class: 'form-control bridge-offset'
|
|
200
|
+
}).attr('step', 'any').val(mapping.offset), { width: '100px' });
|
|
201
|
+
|
|
202
|
+
const timeoutInput = createLabeledField(midRow, 'timeout', $('<input/>', {
|
|
203
|
+
type: 'number',
|
|
204
|
+
class: 'form-control bridge-timeout'
|
|
205
|
+
}).attr('placeholder', node._('knxUltimateIoTBridge.mapping.timeout')).val(mapping.timeout), { width: '140px' });
|
|
206
|
+
|
|
207
|
+
const retryInput = createLabeledField(midRow, 'retry', $('<input/>', {
|
|
208
|
+
type: 'number',
|
|
209
|
+
class: 'form-control bridge-retry'
|
|
210
|
+
}).attr('placeholder', node._('knxUltimateIoTBridge.mapping.retry')).val(mapping.retry), { width: '110px' });
|
|
211
|
+
|
|
212
|
+
const templateInput = createLabeledField(bottomRow, 'template', $('<input/>', {
|
|
213
|
+
type: 'text',
|
|
214
|
+
class: 'form-control bridge-template'
|
|
215
|
+
}).attr('placeholder', node._('knxUltimateIoTBridge.placeholders.template')).val(mapping.template), { flex: '1 1 340px' });
|
|
216
|
+
|
|
217
|
+
const propertyInput = createLabeledField(bottomRow, 'property', $('<input/>', {
|
|
218
|
+
type: 'text',
|
|
219
|
+
class: 'form-control bridge-property'
|
|
220
|
+
}).attr('placeholder', node._('knxUltimateIoTBridge.placeholders.property')).val(mapping.property), { flex: '1 1 260px' });
|
|
221
|
+
|
|
222
|
+
const updateChannelPresentation = (channel) => {
|
|
223
|
+
const methodWrapper = methodInput.data('bridgeWrapper');
|
|
224
|
+
const modbusWrapper = modbusInput.data('bridgeWrapper');
|
|
225
|
+
const targetLabel = targetInput.data('bridgeLabel');
|
|
226
|
+
const methodLabel = methodInput.data('bridgeLabel');
|
|
227
|
+
const modbusLabel = modbusInput.data('bridgeLabel');
|
|
228
|
+
|
|
229
|
+
const translateVariant = (group, variant, fallbackFieldKey) => {
|
|
230
|
+
let text = node._(`knxUltimateIoTBridge.fieldVariants.${group}.${variant}`);
|
|
231
|
+
if (!text || text.indexOf('??') !== -1) {
|
|
232
|
+
text = node._(`knxUltimateIoTBridge.fieldVariants.${group}.default`);
|
|
233
|
+
if (!text || text.indexOf('??') !== -1) {
|
|
234
|
+
text = node._(`knxUltimateIoTBridge.fields.${fallbackFieldKey}`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return text;
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
const translatePlaceholder = (baseKey, variant) => {
|
|
241
|
+
let placeholder = node._(`knxUltimateIoTBridge.placeholders.${baseKey}_${variant}`);
|
|
242
|
+
if (!placeholder || placeholder.indexOf('??') !== -1) {
|
|
243
|
+
placeholder = node._(`knxUltimateIoTBridge.placeholders.${baseKey}`);
|
|
244
|
+
}
|
|
245
|
+
return placeholder;
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
if (channel === 'rest') {
|
|
249
|
+
methodWrapper.show();
|
|
250
|
+
} else {
|
|
251
|
+
methodWrapper.hide();
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (channel === 'modbus') {
|
|
255
|
+
modbusWrapper.show();
|
|
256
|
+
} else {
|
|
257
|
+
modbusWrapper.hide();
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
targetLabel.text(translateVariant('target', channel, 'target'));
|
|
261
|
+
targetInput.attr('placeholder', translatePlaceholder('target', channel));
|
|
262
|
+
|
|
263
|
+
methodLabel.text(translateVariant('method', channel, 'method'));
|
|
264
|
+
modbusLabel.text(translateVariant('modbusFunction', channel, 'modbusFunction'));
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
typeSelect.on('change', function () {
|
|
268
|
+
updateChannelPresentation($(this).val());
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
updateChannelPresentation(typeSelect.val());
|
|
272
|
+
|
|
273
|
+
row.data('mapping-id', mapping.id || '');
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
(node.mappings || []).forEach((m) => {
|
|
278
|
+
container.editableList('addItem', { mapping: m });
|
|
279
|
+
});
|
|
280
|
+
},
|
|
281
|
+
oneditsave: function () {
|
|
282
|
+
const node = this;
|
|
283
|
+
const items = $('#node-input-mapping-container').editableList('items');
|
|
284
|
+
node.mappings = [];
|
|
285
|
+
items.each(function () {
|
|
286
|
+
const row = $(this);
|
|
287
|
+
node.mappings.push({
|
|
288
|
+
id: row.data('mapping-id') || '',
|
|
289
|
+
enabled: row.find('.bridge-enabled').is(':checked'),
|
|
290
|
+
label: row.find('.bridge-label').val(),
|
|
291
|
+
ga: row.find('.bridge-ga').val(),
|
|
292
|
+
dpt: row.find('.bridge-dpt').val(),
|
|
293
|
+
direction: row.find('.bridge-direction').val(),
|
|
294
|
+
iotType: row.find('.bridge-type').val(),
|
|
295
|
+
target: row.find('.bridge-target').val(),
|
|
296
|
+
method: row.find('.bridge-method').val(),
|
|
297
|
+
modbusFunction: row.find('.bridge-modbus').val(),
|
|
298
|
+
scale: row.find('.bridge-scale').val(),
|
|
299
|
+
offset: row.find('.bridge-offset').val(),
|
|
300
|
+
timeout: row.find('.bridge-timeout').val(),
|
|
301
|
+
retry: row.find('.bridge-retry').val(),
|
|
302
|
+
template: row.find('.bridge-template').val(),
|
|
303
|
+
property: row.find('.bridge-property').val()
|
|
304
|
+
});
|
|
305
|
+
});
|
|
306
|
+
try {
|
|
307
|
+
RED.sidebar.show('info');
|
|
308
|
+
} catch (error) { }
|
|
309
|
+
},
|
|
310
|
+
oneditresize: function (size) {
|
|
311
|
+
const rows = $('#dialog-form>div:not(.node-input-mapping-container-row)');
|
|
312
|
+
let height = size.height;
|
|
313
|
+
for (let i = 0; i < rows.length; i++) {
|
|
314
|
+
height -= $(rows[i]).outerHeight(true);
|
|
315
|
+
}
|
|
316
|
+
const editorRow = $('#dialog-form>div.node-input-mapping-container-row');
|
|
317
|
+
height -= (parseInt(editorRow.css('marginTop')) + parseInt(editorRow.css('marginBottom')));
|
|
318
|
+
height += 16;
|
|
319
|
+
$('#node-input-mapping-container').editableList('height', height);
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
</script>
|
|
323
|
+
|
|
324
|
+
<script type="text/html" data-template-name="knxUltimateIoTBridge">
|
|
325
|
+
<div class="form-row">
|
|
326
|
+
<label for="node-input-server" data-i18n="knxUltimateIoTBridge.node-input-server"></label>
|
|
327
|
+
<input type="text" id="node-input-server" />
|
|
328
|
+
</div>
|
|
329
|
+
<div class="form-row">
|
|
330
|
+
<label for="node-input-name" data-i18n="knxUltimateIoTBridge.node-input-name"></label>
|
|
331
|
+
<input type="text" id="node-input-name" />
|
|
332
|
+
</div>
|
|
333
|
+
<div class="form-row">
|
|
334
|
+
<label for="node-input-outputtopic" data-i18n="knxUltimateIoTBridge.node-input-outputtopic"></label>
|
|
335
|
+
<input type="text" id="node-input-outputtopic" />
|
|
336
|
+
</div>
|
|
337
|
+
<div class="form-row" style="display:flex; align-items:flex-start; gap:8px;">
|
|
338
|
+
<input type="checkbox" id="node-input-emitOnChangeOnly" style="width:auto; margin-top:4px;" />
|
|
339
|
+
<label for="node-input-emitOnChangeOnly" style="flex:1; margin:0;">
|
|
340
|
+
<span data-i18n="knxUltimateIoTBridge.node-input-emitOnChangeOnly"></span>
|
|
341
|
+
</label>
|
|
342
|
+
</div>
|
|
343
|
+
<div class="form-row" style="display:flex; align-items:flex-start; gap:8px;">
|
|
344
|
+
<input type="checkbox" id="node-input-readOnDeploy" style="width:auto; margin-top:4px;" />
|
|
345
|
+
<label for="node-input-readOnDeploy" style="flex:1; margin:0;">
|
|
346
|
+
<span data-i18n="knxUltimateIoTBridge.node-input-readOnDeploy"></span>
|
|
347
|
+
</label>
|
|
348
|
+
</div>
|
|
349
|
+
<div class="form-row" style="display:flex; align-items:flex-start; gap:8px;">
|
|
350
|
+
<input type="checkbox" id="node-input-acceptFlowInput" style="width:auto; margin-top:4px;" />
|
|
351
|
+
<label for="node-input-acceptFlowInput" style="flex:1; margin:0;">
|
|
352
|
+
<span data-i18n="knxUltimateIoTBridge.node-input-acceptFlowInput"></span>
|
|
353
|
+
</label>
|
|
354
|
+
</div>
|
|
355
|
+
<div class="form-row node-input-mapping-container-row">
|
|
356
|
+
<label style="width:auto;" data-i18n="knxUltimateIoTBridge.section_mappings"></label>
|
|
357
|
+
<ol id="node-input-mapping-container"></ol>
|
|
358
|
+
</div>
|
|
359
|
+
<br/><br/><br/><br/>
|
|
360
|
+
</script>
|
|
361
|
+
|
|
362
|
+
<script type="text/markdown" data-help-name="knxUltimateIoTBridge">
|
|
363
|
+
## KNX ↔ IoT Bridge
|
|
364
|
+
|
|
365
|
+
Configure bidirectional maps between KNX group addresses and IoT backends such as MQTT, REST APIs or Modbus registers. Each mapping can scale values, format payloads and define single direction behaviour.
|
|
366
|
+
|
|
367
|
+
### Inputs
|
|
368
|
+
- **Flow input**: when enabled, a message whose `topic` (or `msg.bridge`) matches a configured mapping is converted to the KNX payload and written to the bus.
|
|
369
|
+
- **KNX telegrams**: received automatically from the configured gateway and routed through the mapping list.
|
|
370
|
+
|
|
371
|
+
### Outputs
|
|
372
|
+
- **Output 1 (KNX → IoT)**: emits a message with the mapped payload plus metadata in `msg.bridge` and `msg.knx`.
|
|
373
|
+
- **Output 2 (IoT → KNX ack)**: reports when a flow message has been written to KNX, including the resolved GA and scaling information.
|
|
374
|
+
|
|
375
|
+
### Mapping options
|
|
376
|
+
- **Direction**: choose between KNX→IoT, IoT→KNX or bidirectional.
|
|
377
|
+
- **Channel type**: pick MQTT, REST or Modbus. The `target` field adapts: topic name, URL or register address.
|
|
378
|
+
- **Template**: optional string; placeholders `{{value}}`, `{{ga}}`, `{{target}}`, `{{label}}`, `{{type}}`, `{{isoTimestamp}}` are replaced at runtime.
|
|
379
|
+
- **Scale & Offset**: numeric transformation applied on KNX→IoT. For IoT→KNX the inverse is used.
|
|
380
|
+
- **Timeout/Retries**: retained for flow logic; the node does not execute external requests but exposes the desired values to downstream nodes.
|
|
381
|
+
|
|
382
|
+
### Tips
|
|
383
|
+
- Place HTTP or MQTT nodes after the bridge outputs to perform the actual transport.
|
|
384
|
+
- Use `msg.bridge.id` to route acknowledgements or correlate responses.
|
|
385
|
+
- Enable "Read KNX values on deploy" to bootstrap dashboards after deploys.
|
|
386
|
+
|
|
387
|
+
</script>
|