node-red-contrib-knx-ultimate 4.0.11 → 4.0.13
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 +11 -0
- package/nodes/commonFunctions.js +7 -1
- package/nodes/knxUltimate-config.html +11 -7
- package/nodes/knxUltimate-config.js +33 -8
- package/nodes/knxUltimate.js +12 -8
- package/nodes/knxUltimateAlerter.js +13 -15
- package/nodes/knxUltimateAutoResponder.js +7 -7
- package/nodes/knxUltimateGarage.html +309 -0
- package/nodes/knxUltimateGarage.js +501 -0
- package/nodes/knxUltimateGlobalContext.js +7 -7
- package/nodes/knxUltimateHueBattery.js +9 -13
- package/nodes/knxUltimateHueButton.js +7 -7
- package/nodes/knxUltimateHueCameraMotion.js +9 -13
- package/nodes/knxUltimateHueContactSensor.js +9 -13
- package/nodes/knxUltimateHueHumiditySensor.js +9 -13
- package/nodes/knxUltimateHueLight.js +48 -14
- package/nodes/knxUltimateHueLightSensor.js +9 -13
- package/nodes/knxUltimateHueMotion.js +9 -13
- package/nodes/knxUltimateHuePlug.js +7 -7
- package/nodes/knxUltimateHueScene.js +7 -7
- package/nodes/knxUltimateHueTapDial.js +9 -13
- package/nodes/knxUltimateHueTemperatureSensor.js +9 -13
- package/nodes/knxUltimateHueZigbeeConnectivity.js +9 -13
- package/nodes/knxUltimateHuedevice_software_update.js +10 -16
- package/nodes/knxUltimateLoadControl.js +7 -7
- package/nodes/knxUltimateLogger.js +7 -7
- package/nodes/knxUltimateSceneController.js +7 -7
- package/nodes/knxUltimateStaircase.html +326 -0
- package/nodes/knxUltimateStaircase.js +461 -0
- package/nodes/knxUltimateViewer.js +15 -15
- package/nodes/knxUltimateWatchDog.js +7 -7
- package/nodes/locales/en/knxUltimate-config.html +1 -1
- package/nodes/locales/en/knxUltimate-config.json +7 -3
- package/nodes/locales/en/knxUltimateGarage.html +62 -0
- package/nodes/locales/en/knxUltimateGarage.json +48 -0
- package/nodes/locales/en/knxUltimateStaircase.html +66 -0
- package/nodes/locales/en/knxUltimateStaircase.json +59 -0
- package/nodes/locales/es/knxUltimate-config.html +1 -1
- package/nodes/locales/es/knxUltimate-config.json +7 -3
- package/nodes/locales/es/knxUltimateGarage.html +63 -0
- package/nodes/locales/es/knxUltimateGarage.json +48 -0
- package/nodes/locales/es/knxUltimateLogger.html +1 -1
- package/nodes/locales/es/knxUltimateSceneController.html +1 -1
- package/nodes/locales/es/knxUltimateStaircase.html +66 -0
- package/nodes/locales/es/knxUltimateStaircase.json +59 -0
- package/nodes/locales/es/knxUltimateWatchDog.html +1 -1
- package/nodes/locales/fr/knxUltimate-config.html +1 -1
- package/nodes/locales/fr/knxUltimate-config.json +7 -3
- package/nodes/locales/fr/knxUltimateGarage.html +63 -0
- package/nodes/locales/fr/knxUltimateGarage.json +48 -0
- package/nodes/locales/fr/knxUltimateStaircase.html +66 -0
- package/nodes/locales/fr/knxUltimateStaircase.json +59 -0
- package/nodes/locales/it/knxUltimate-config.html +1 -1
- package/nodes/locales/it/knxUltimate-config.json +7 -3
- package/nodes/locales/it/knxUltimateGarage.html +63 -0
- package/nodes/locales/it/knxUltimateGarage.json +48 -0
- package/nodes/locales/it/knxUltimateStaircase.html +66 -0
- package/nodes/locales/it/knxUltimateStaircase.json +59 -0
- package/package.json +4 -2
- package/tutorial/knxUltimate-AllNodes-Presentazione.md +190 -0
- package/tutorial/knxUltimateHueBattery-teleprompter.txt +5 -1
- package/tutorial/knxUltimateHueBattery.md +8 -5
- package/tutorial/knxUltimateHueCameraMotion-teleprompter.txt +44 -0
- package/tutorial/knxUltimateHueCameraMotion.md +45 -0
- package/tutorial/knxUltimateHueHumiditySensor-teleprompter.txt +43 -0
- package/tutorial/knxUltimateHueHumiditySensor.md +45 -0
- package/tutorial/knxUltimateHueLightSensor-teleprompter.txt +4 -0
- package/tutorial/knxUltimateHueLightSensor.md +5 -5
- package/tutorial/knxUltimateHueMotion-teleprompter.txt +4 -0
- package/tutorial/knxUltimateHueMotion.md +8 -4
- package/tutorial/knxUltimateHuePlug-teleprompter.txt +48 -0
- package/tutorial/knxUltimateHuePlug.md +49 -0
- package/tutorial/knxUltimateHueScene-teleprompter.txt +4 -0
- package/tutorial/knxUltimateHueTapDial-teleprompter.txt +4 -0
- package/tutorial/knxUltimateHueTapDial.md +8 -4
- package/tutorial/knxUltimateHueTemperatureSensor-teleprompter.txt +4 -0
- package/tutorial/knxUltimateHueTemperatureSensor.md +5 -4
- package/tutorial/knxUltimateHueZigbeeConnectivity-teleprompter.txt +4 -0
- package/tutorial/knxUltimateHueZigbeeConnectivity.md +4 -3
- package/tutorial/knxUltimateHuedevice_software_update-teleprompter.txt +4 -0
- package/tutorial/knxUltimateHuedevice_software_update.md +5 -5
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,17 @@
|
|
|
6
6
|
|
|
7
7
|
# CHANGELOG
|
|
8
8
|
|
|
9
|
+
**Version 4.0.13** - October 2025<br/>
|
|
10
|
+
- NEW: Added KNX Garage Door node with boolean/impulse control, hold-open and disable GAs, safety integration and automatic re-close timer.<br/>
|
|
11
|
+
- KNX Staircase node: editor refinements with read-only DPTs, flow payload support and localisation polish across supported languages.<br/>
|
|
12
|
+
- KNX Staircase & Garage nodes: unified status handling and multilingual flow examples in docs/help.<br/>
|
|
13
|
+
|
|
14
|
+
**Version 4.0.12** - October 2025<br/>
|
|
15
|
+
- NEW: Added KNX Staircase Light node with timer handling, pre-warning, override and block support.<br/>
|
|
16
|
+
- KNX Config node: replaced the "errors only" status filter with a configurable status throttle (0/1/3/5/10/30 s) that emits only the latest status after the chosen delay, preventing editor memory growth when many nodes update.<br/>
|
|
17
|
+
- HUE Light node: the "Keep brightness" option now restores the last active dim level when toggled via KNX On/Off instead of forcing 100%.<br/>
|
|
18
|
+
- Common functions: hardened `/knxultimateCheckHueConnected` admin endpoint so missing/initialising HUE config nodes no longer raise exceptions.<br/>
|
|
19
|
+
|
|
9
20
|
**Version 4.0.11** - October 2025<br/>
|
|
10
21
|
- HUE nodes: hardened KNX telegram handling with a shared safe-send guard so editor events no longer ceases to function, when the KNX gateway is offline.<br/>
|
|
11
22
|
- HUE Contact Sensor node: placeholders now leverage i18n translations with graceful fallback when translation keys are missing.<br/>
|
package/nodes/commonFunctions.js
CHANGED
|
@@ -74,10 +74,16 @@ module.exports = (RED) => {
|
|
|
74
74
|
RED.httpAdmin.get('/knxultimateCheckHueConnected', (req, res) => {
|
|
75
75
|
try {
|
|
76
76
|
const serverId = RED.nodes.getNode(req.query.serverId); // Retrieve node.id of the config node.
|
|
77
|
+
if (!serverId) {
|
|
78
|
+
res.json({ ready: false });
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
77
81
|
if (serverId.hueAllResources === null || serverId.hueAllResources === undefined) {
|
|
78
82
|
(async function main() {
|
|
79
83
|
try {
|
|
80
|
-
|
|
84
|
+
if (typeof serverId.loadResourcesFromHUEBridge === 'function') {
|
|
85
|
+
await serverId.loadResourcesFromHUEBridge();
|
|
86
|
+
}
|
|
81
87
|
} catch (error) {
|
|
82
88
|
RED.log.error(`Errore RED.httpAdmin.get('/knxultimateCheckHueConnected' ${error.stack}`);
|
|
83
89
|
}
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
tunnelUserPassword: { value: "" },
|
|
29
29
|
tunnelUserId: { value: "" },
|
|
30
30
|
autoReconnect: { value: "yes" },
|
|
31
|
-
|
|
31
|
+
statusUpdateThrottle: { value: "0" }
|
|
32
32
|
},
|
|
33
33
|
credentials: {
|
|
34
34
|
keyringFilePassword: { type: "password" }
|
|
@@ -1148,13 +1148,17 @@
|
|
|
1148
1148
|
</div>
|
|
1149
1149
|
|
|
1150
1150
|
<div class="form-row">
|
|
1151
|
-
<label for="node-config-input-
|
|
1152
|
-
<i class="fa fa-
|
|
1153
|
-
<span data-i18n="knxUltimate-config.advanced.
|
|
1151
|
+
<label for="node-config-input-statusUpdateThrottle">
|
|
1152
|
+
<i class="fa fa-clock-o"></i>
|
|
1153
|
+
<span data-i18n="knxUltimate-config.advanced.status_throttle"></span>
|
|
1154
1154
|
</label>
|
|
1155
|
-
<select id="node-config-input-
|
|
1156
|
-
<option value="
|
|
1157
|
-
<option value="
|
|
1155
|
+
<select id="node-config-input-statusUpdateThrottle" style="width:40%;">
|
|
1156
|
+
<option value="0" data-i18n="knxUltimate-config.advanced.status_throttle_none"></option>
|
|
1157
|
+
<option value="1" data-i18n="knxUltimate-config.advanced.status_throttle_1s"></option>
|
|
1158
|
+
<option value="3" data-i18n="knxUltimate-config.advanced.status_throttle_3s"></option>
|
|
1159
|
+
<option value="5" data-i18n="knxUltimate-config.advanced.status_throttle_5s"></option>
|
|
1160
|
+
<option value="10" data-i18n="knxUltimate-config.advanced.status_throttle_10s"></option>
|
|
1161
|
+
<option value="30" data-i18n="knxUltimate-config.advanced.status_throttle_30s"></option>
|
|
1158
1162
|
</select>
|
|
1159
1163
|
</div>
|
|
1160
1164
|
</p>
|
|
@@ -24,8 +24,6 @@ const loggerClass = require('./utils/sysLogger')
|
|
|
24
24
|
const payloadRounder = require("./utils/payloadManipulation");
|
|
25
25
|
const utils = require('./utils/utils');
|
|
26
26
|
|
|
27
|
-
const STATUS_DISPLAY_ALLOWED_COLORS = new Set(['red', 'yellow']);
|
|
28
|
-
|
|
29
27
|
// DATAPONT MANIPULATION HELPERS
|
|
30
28
|
// ####################
|
|
31
29
|
const sortBy = (field) => (a, b) => {
|
|
@@ -220,12 +218,39 @@ module.exports = (RED) => {
|
|
|
220
218
|
node.autoReconnect = true;
|
|
221
219
|
}
|
|
222
220
|
node.ignoreTelegramsWithRepeatedFlag = config.ignoreTelegramsWithRepeatedFlag === undefined ? false : config.ignoreTelegramsWithRepeatedFlag;
|
|
223
|
-
const
|
|
224
|
-
node.
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
221
|
+
const throttleSecondsRaw = Number(config.statusUpdateThrottle);
|
|
222
|
+
node.statusUpdateThrottleMs = Number.isFinite(throttleSecondsRaw) && throttleSecondsRaw > 0
|
|
223
|
+
? throttleSecondsRaw * 1000
|
|
224
|
+
: 0;
|
|
225
|
+
node.applyStatusUpdate = (targetNode, status) => {
|
|
226
|
+
try {
|
|
227
|
+
if (!targetNode || typeof targetNode.status !== 'function') return;
|
|
228
|
+
const throttle = node.statusUpdateThrottleMs;
|
|
229
|
+
if (!throttle) {
|
|
230
|
+
targetNode.status(status);
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
if (!targetNode.__knxStatusThrottle) {
|
|
234
|
+
targetNode.__knxStatusThrottle = { pending: undefined, timer: null };
|
|
235
|
+
}
|
|
236
|
+
const tracker = targetNode.__knxStatusThrottle;
|
|
237
|
+
tracker.pending = status;
|
|
238
|
+
if (tracker.timer) return;
|
|
239
|
+
tracker.timer = setTimeout(() => {
|
|
240
|
+
try {
|
|
241
|
+
if (tracker.pending !== undefined) {
|
|
242
|
+
targetNode.status(tracker.pending);
|
|
243
|
+
}
|
|
244
|
+
} catch (timerError) {
|
|
245
|
+
node.sysLogger?.warn('Unable to apply throttled status: ' + timerError.message);
|
|
246
|
+
} finally {
|
|
247
|
+
tracker.pending = undefined;
|
|
248
|
+
tracker.timer = null;
|
|
249
|
+
}
|
|
250
|
+
}, throttle);
|
|
251
|
+
} catch (error) {
|
|
252
|
+
node.sysLogger?.warn('applyStatusUpdate error: ' + error.message);
|
|
253
|
+
}
|
|
229
254
|
};
|
|
230
255
|
// 24/07/2021 KNX Secure checks...
|
|
231
256
|
node.keyringFileXML = typeof config.keyringFileXML === "undefined" || config.keyringFileXML.trim() === "" ? "" : config.keyringFileXML;
|
package/nodes/knxUltimate.js
CHANGED
|
@@ -11,8 +11,17 @@ module.exports = function (RED) {
|
|
|
11
11
|
RED.nodes.createNode(this, config);
|
|
12
12
|
const node = this;
|
|
13
13
|
node.serverKNX = RED.nodes.getNode(config.server) || undefined;
|
|
14
|
+
const pushStatus = (status) => {
|
|
15
|
+
const provider = node.serverKNX;
|
|
16
|
+
if (provider && typeof provider.applyStatusUpdate === 'function') {
|
|
17
|
+
provider.applyStatusUpdate(node, status);
|
|
18
|
+
} else {
|
|
19
|
+
node.status(status);
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
|
|
14
23
|
if (node.serverKNX === undefined) {
|
|
15
|
-
|
|
24
|
+
pushStatus({ fill: 'red', shape: 'dot', text: '[THE GATEWAY NODE HAS BEEN DISABLED]' });
|
|
16
25
|
return;
|
|
17
26
|
}
|
|
18
27
|
|
|
@@ -22,7 +31,7 @@ module.exports = function (RED) {
|
|
|
22
31
|
fill, shape, text, payload, GA, dpt, devicename,
|
|
23
32
|
}) => {
|
|
24
33
|
try {
|
|
25
|
-
if (node.serverKNX === null) {
|
|
34
|
+
if (node.serverKNX === null) { pushStatus({ fill: 'red', shape: 'dot', text: '[NO GATEWAY SELECTED]' }); return; }
|
|
26
35
|
if (node.icountMessageInWindow == -999) return; // Locked out, doesn't change status.
|
|
27
36
|
const dDate = new Date();
|
|
28
37
|
// 30/08/2019 Display only the things selected in the config
|
|
@@ -31,12 +40,7 @@ module.exports = function (RED) {
|
|
|
31
40
|
dpt = (typeof dpt === 'undefined' || dpt == '') ? '' : ` DPT${dpt}`;
|
|
32
41
|
payload = typeof payload === 'object' ? JSON.stringify(payload) : payload;
|
|
33
42
|
const statusText = `${GA + payload + (node.listenallga === true ? ` ${devicename}` : '')} (day ${dDate.getDate()}, ${dDate.toLocaleTimeString()}) ${text}`;
|
|
34
|
-
|
|
35
|
-
? node.serverKNX.shouldDisplayStatus(fill)
|
|
36
|
-
: true;
|
|
37
|
-
if (shouldUpdateStatus) {
|
|
38
|
-
node.status({ fill, shape, text: statusText });
|
|
39
|
-
}
|
|
43
|
+
pushStatus({ fill, shape, text: statusText });
|
|
40
44
|
// 16/02/2020 signal errors to the server
|
|
41
45
|
if (fill.toUpperCase() === 'RED') {
|
|
42
46
|
if (node.serverKNX) {
|
|
@@ -11,8 +11,18 @@ module.exports = function (RED) {
|
|
|
11
11
|
RED.nodes.createNode(this, config);
|
|
12
12
|
const node = this;
|
|
13
13
|
node.serverKNX = RED.nodes.getNode(config.server) || undefined;
|
|
14
|
+
const pushStatus = (status) => {
|
|
15
|
+
if (status === undefined || status === null) return;
|
|
16
|
+
const provider = node.serverKNX;
|
|
17
|
+
if (provider && typeof provider.applyStatusUpdate === 'function') {
|
|
18
|
+
provider.applyStatusUpdate(node, status);
|
|
19
|
+
} else {
|
|
20
|
+
node.status(status);
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
|
|
14
24
|
if (node.serverKNX === undefined) {
|
|
15
|
-
|
|
25
|
+
pushStatus({ fill: 'red', shape: 'dot', text: '[THE GATEWAY NODE HAS BEEN DISABLED]' });
|
|
16
26
|
return;
|
|
17
27
|
}
|
|
18
28
|
node.name = config.name || 'KNX Alerter';
|
|
@@ -33,14 +43,6 @@ module.exports = function (RED) {
|
|
|
33
43
|
node.whentostart = config.whentostart === undefined ? 'ifnewalert' : config.whentostart;
|
|
34
44
|
node.timerinterval = (config.timerinterval === undefined || config.timerinterval == '') ? '2' : config.timerinterval;
|
|
35
45
|
|
|
36
|
-
const shouldDisplayStatus = (color) => {
|
|
37
|
-
const provider = node.serverKNX;
|
|
38
|
-
if (provider && typeof provider.shouldDisplayStatus === 'function') {
|
|
39
|
-
return provider.shouldDisplayStatus(color);
|
|
40
|
-
}
|
|
41
|
-
return true;
|
|
42
|
-
};
|
|
43
|
-
|
|
44
46
|
if (config.initialreadGAInRules === undefined) {
|
|
45
47
|
node.initialread = 1;
|
|
46
48
|
} else {
|
|
@@ -61,9 +63,7 @@ module.exports = function (RED) {
|
|
|
61
63
|
devicename = devicename || '';
|
|
62
64
|
dpt = (typeof dpt === 'undefined' || dpt == '') ? '' : ' DPT' + dpt;
|
|
63
65
|
payload = typeof payload === 'object' ? JSON.stringify(payload) : payload;
|
|
64
|
-
|
|
65
|
-
node.status({ fill, shape, text: GA + payload + (node.listenallga === true ? ' ' + devicename : '') + ' (' + dDate.getDate() + ', ' + dDate.toLocaleTimeString() + ' ' + text });
|
|
66
|
-
}
|
|
66
|
+
pushStatus({ fill, shape, text: GA + payload + (node.listenallga === true ? ' ' + devicename : '') + ' (' + dDate.getDate() + ', ' + dDate.toLocaleTimeString() + ' ' + text });
|
|
67
67
|
} catch (error) {
|
|
68
68
|
}
|
|
69
69
|
};
|
|
@@ -76,9 +76,7 @@ module.exports = function (RED) {
|
|
|
76
76
|
devicename = devicename || '';
|
|
77
77
|
dpt = (typeof dpt === 'undefined' || dpt == '') ? '' : ' DPT' + dpt;
|
|
78
78
|
try {
|
|
79
|
-
|
|
80
|
-
node.status({ fill, shape, text: GA + payload + (node.listenallga === true ? ' ' + devicename : '') + ' (' + dDate.getDate() + ', ' + dDate.toLocaleTimeString() + ' ' + text });
|
|
81
|
-
}
|
|
79
|
+
pushStatus({ fill, shape, text: GA + payload + (node.listenallga === true ? ' ' + devicename : '') + ' (' + dDate.getDate() + ', ' + dDate.toLocaleTimeString() + ' ' + text });
|
|
82
80
|
} catch (error) {
|
|
83
81
|
}
|
|
84
82
|
};
|
|
@@ -52,19 +52,19 @@ module.exports = function (RED) {
|
|
|
52
52
|
node.commandText = []; // Raw list Respond To
|
|
53
53
|
node.timerSaveExposedGAs = null;
|
|
54
54
|
|
|
55
|
-
const
|
|
55
|
+
const pushStatus = (status) => {
|
|
56
|
+
if (!status) return;
|
|
56
57
|
const provider = node.serverKNX;
|
|
57
|
-
if (provider && typeof provider.
|
|
58
|
-
|
|
58
|
+
if (provider && typeof provider.applyStatusUpdate === 'function') {
|
|
59
|
+
provider.applyStatusUpdate(node, status);
|
|
60
|
+
} else {
|
|
61
|
+
node.status(status);
|
|
59
62
|
}
|
|
60
|
-
return true;
|
|
61
63
|
};
|
|
62
64
|
|
|
63
65
|
const updateStatus = (status) => {
|
|
64
66
|
if (!status) return;
|
|
65
|
-
|
|
66
|
-
node.status(status);
|
|
67
|
-
}
|
|
67
|
+
pushStatus(status);
|
|
68
68
|
};
|
|
69
69
|
if (node.serverKNX === null) { updateStatus({ fill: 'red', shape: 'dot', text: '[NO GATEWAY SELECTED]' }); return; }
|
|
70
70
|
|
|
@@ -0,0 +1,309 @@
|
|
|
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('knxUltimateGarage', {
|
|
5
|
+
category: 'KNX Ultimate',
|
|
6
|
+
color: '#7dd484',
|
|
7
|
+
defaults: {
|
|
8
|
+
server: { type: 'knxUltimate-config', required: true },
|
|
9
|
+
name: { value: '' },
|
|
10
|
+
outputtopic: { value: '' },
|
|
11
|
+
gaCommand: { value: '', required: true },
|
|
12
|
+
nameCommand: { value: '' },
|
|
13
|
+
dptCommand: { value: '1.001' },
|
|
14
|
+
gaImpulse: { value: '' },
|
|
15
|
+
nameImpulse: { value: '' },
|
|
16
|
+
dptImpulse: { value: '1.017' },
|
|
17
|
+
gaHoldOpen: { value: '' },
|
|
18
|
+
nameHoldOpen: { value: '' },
|
|
19
|
+
dptHoldOpen: { value: '1.001' },
|
|
20
|
+
gaDisable: { value: '' },
|
|
21
|
+
nameDisable: { value: '' },
|
|
22
|
+
dptDisable: { value: '1.001' },
|
|
23
|
+
gaPhotocell: { value: '' },
|
|
24
|
+
namePhotocell: { value: '' },
|
|
25
|
+
dptPhotocell: { value: '1.001' },
|
|
26
|
+
gaMoving: { value: '' },
|
|
27
|
+
nameMoving: { value: '' },
|
|
28
|
+
dptMoving: { value: '1.001' },
|
|
29
|
+
gaObstruction: { value: '' },
|
|
30
|
+
nameObstruction: { value: '' },
|
|
31
|
+
dptObstruction: { value: '1.001' },
|
|
32
|
+
autoCloseEnable: { value: true },
|
|
33
|
+
autoCloseSeconds: { value: 120, validate: RED.validators.number() },
|
|
34
|
+
emitEvents: { value: false }
|
|
35
|
+
},
|
|
36
|
+
inputs: 1,
|
|
37
|
+
outputs: 1,
|
|
38
|
+
icon: 'node-knx-icon.svg',
|
|
39
|
+
label: function () {
|
|
40
|
+
return this.name || 'KNX Garage';
|
|
41
|
+
},
|
|
42
|
+
paletteLabel: 'KNX Garage',
|
|
43
|
+
oneditprepare: function () {
|
|
44
|
+
const node = this;
|
|
45
|
+
const $knxServerInput = $('#node-input-server');
|
|
46
|
+
try { RED.sidebar.show('help'); } catch (error) { /* ignore */ }
|
|
47
|
+
const KNX_EMPTY_VALUES = new Set(['', '_ADD_', '__NONE__', 'none']);
|
|
48
|
+
|
|
49
|
+
const resolveKnxServerValue = () => {
|
|
50
|
+
const domValue = $knxServerInput.val();
|
|
51
|
+
if (domValue !== undefined && domValue !== null && domValue !== '') return domValue;
|
|
52
|
+
if (node.server !== undefined && node.server !== null && node.server !== '') return node.server;
|
|
53
|
+
return '';
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const hasKnxServerSelected = () => {
|
|
57
|
+
const val = resolveKnxServerValue();
|
|
58
|
+
return !(val === undefined || val === null || KNX_EMPTY_VALUES.has(String(val)));
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const KNX_GA_CACHE = node._knxGaCache || (node._knxGaCache = new Map());
|
|
62
|
+
|
|
63
|
+
const fetchGroupAddresses = (serverId) => {
|
|
64
|
+
if (!serverId) return Promise.resolve([]);
|
|
65
|
+
if (KNX_GA_CACHE.has(serverId)) return Promise.resolve(KNX_GA_CACHE.get(serverId));
|
|
66
|
+
return new Promise((resolve) => {
|
|
67
|
+
$.getJSON(`knxUltimatecsv?nodeID=${serverId}&_=${Date.now()}`, (data) => {
|
|
68
|
+
const list = Array.isArray(data) ? data : [];
|
|
69
|
+
KNX_GA_CACHE.set(serverId, list);
|
|
70
|
+
resolve(list);
|
|
71
|
+
}).fail(() => resolve([]));
|
|
72
|
+
});
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const getGroupAddress = (gaSelector, nameSelector, dptSelector, prefixes) => {
|
|
76
|
+
const $gaInput = $(gaSelector);
|
|
77
|
+
const $nameInput = $(nameSelector);
|
|
78
|
+
const $dptInput = $(dptSelector);
|
|
79
|
+
if (!$gaInput.length) return;
|
|
80
|
+
|
|
81
|
+
const ensureAutocomplete = () => {
|
|
82
|
+
const sourceFn = (request, response) => {
|
|
83
|
+
if (!hasKnxServerSelected()) {
|
|
84
|
+
response([]);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const serverId = resolveKnxServerValue();
|
|
88
|
+
fetchGroupAddresses(serverId).then((data) => {
|
|
89
|
+
const items = [];
|
|
90
|
+
data.forEach((entry) => {
|
|
91
|
+
const dpt = entry.dpt || '';
|
|
92
|
+
const allowed = prefixes.some((prefix) => prefix === '' || dpt.startsWith(prefix));
|
|
93
|
+
if (!allowed) return;
|
|
94
|
+
const devName = entry.devicename || '';
|
|
95
|
+
const searchStr = `${entry.ga} (${devName}) DPT${dpt}`;
|
|
96
|
+
if (!htmlUtilsfullCSVSearch(searchStr, request.term || '')) return;
|
|
97
|
+
items.push({
|
|
98
|
+
label: `${entry.ga} # ${devName} # ${dpt}`,
|
|
99
|
+
value: entry.ga,
|
|
100
|
+
dpt
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
response(items);
|
|
104
|
+
});
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
if ($gaInput.data('knx-ga-initialised')) {
|
|
108
|
+
$gaInput.autocomplete('option', 'source', sourceFn);
|
|
109
|
+
} else {
|
|
110
|
+
$gaInput
|
|
111
|
+
.autocomplete({
|
|
112
|
+
minLength: 0,
|
|
113
|
+
source: sourceFn,
|
|
114
|
+
select: (event, ui) => {
|
|
115
|
+
let deviceName = '';
|
|
116
|
+
try {
|
|
117
|
+
deviceName = ui.item.label.split('#')[1].trim();
|
|
118
|
+
deviceName = deviceName.replace(/^\)/, '').trim();
|
|
119
|
+
} catch (error) { deviceName = ''; }
|
|
120
|
+
if ($nameInput.length) {
|
|
121
|
+
if (deviceName && deviceName !== '') {
|
|
122
|
+
$nameInput.val(deviceName);
|
|
123
|
+
} else if (!$nameInput.val()) {
|
|
124
|
+
$nameInput.val('');
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
try {
|
|
128
|
+
const parts = ui.item.label.split('#');
|
|
129
|
+
const dptFromLabel = parts.length >= 3 ? parts[2].trim() : '';
|
|
130
|
+
if (dptFromLabel !== '') {
|
|
131
|
+
$dptInput.val(dptFromLabel);
|
|
132
|
+
}
|
|
133
|
+
} catch (error) { /* ignore */ }
|
|
134
|
+
}
|
|
135
|
+
})
|
|
136
|
+
.on('focus.knxUltimateGarage click.knxUltimateGarage', function () {
|
|
137
|
+
const currentValue = $(this).val() || '';
|
|
138
|
+
try { $(this).autocomplete('search', `${currentValue} exactmatch`); } catch (error) { /* ignore */ }
|
|
139
|
+
});
|
|
140
|
+
$gaInput.data('knx-ga-initialised', true);
|
|
141
|
+
}
|
|
142
|
+
try {
|
|
143
|
+
if (hasKnxServerSelected()) {
|
|
144
|
+
const srv = RED.nodes.node(resolveKnxServerValue());
|
|
145
|
+
if (srv && srv.id) KNX_enableSecureFormatting($gaInput, srv.id);
|
|
146
|
+
}
|
|
147
|
+
} catch (error) { /* ignore */ }
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
ensureAutocomplete();
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const BINARY_PREFIX = ['1.'];
|
|
154
|
+
|
|
155
|
+
const refreshKnxBindings = () => {
|
|
156
|
+
if (!hasKnxServerSelected()) return;
|
|
157
|
+
|
|
158
|
+
getGroupAddress('#node-input-gaCommand', '#node-input-nameCommand', '#node-input-dptCommand', BINARY_PREFIX);
|
|
159
|
+
getGroupAddress('#node-input-gaImpulse', '#node-input-nameImpulse', '#node-input-dptImpulse', BINARY_PREFIX);
|
|
160
|
+
getGroupAddress('#node-input-gaHoldOpen', '#node-input-nameHoldOpen', '#node-input-dptHoldOpen', BINARY_PREFIX);
|
|
161
|
+
getGroupAddress('#node-input-gaDisable', '#node-input-nameDisable', '#node-input-dptDisable', BINARY_PREFIX);
|
|
162
|
+
getGroupAddress('#node-input-gaPhotocell', '#node-input-namePhotocell', '#node-input-dptPhotocell', BINARY_PREFIX);
|
|
163
|
+
getGroupAddress('#node-input-gaMoving', '#node-input-nameMoving', '#node-input-dptMoving', BINARY_PREFIX);
|
|
164
|
+
getGroupAddress('#node-input-gaObstruction', '#node-input-nameObstruction', '#node-input-dptObstruction', BINARY_PREFIX);
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
$knxServerInput.on('change', () => {
|
|
168
|
+
KNX_GA_CACHE.clear();
|
|
169
|
+
refreshKnxBindings();
|
|
170
|
+
});
|
|
171
|
+
if (hasKnxServerSelected()) refreshKnxBindings();
|
|
172
|
+
|
|
173
|
+
const syncAutoClose = () => {
|
|
174
|
+
const enabled = $('#node-input-autoCloseEnable').is(':checked');
|
|
175
|
+
const $seconds = $('#node-input-autoCloseSeconds').closest('.form-row');
|
|
176
|
+
$seconds.toggle(enabled);
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
$('#node-input-autoCloseEnable').on('change', syncAutoClose);
|
|
180
|
+
syncAutoClose();
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
</script>
|
|
184
|
+
|
|
185
|
+
<script type="text/html" data-template-name="knxUltimateGarage">
|
|
186
|
+
<div class="form-row" style="display:flex; align-items:center;">
|
|
187
|
+
<label for="node-input-server" style="width:180px"><i class="fa fa-circle-o"></i> <span data-i18n="knxUltimateGarage.node-input-server"></span></label>
|
|
188
|
+
<input type="text" id="node-input-server" style="flex:1">
|
|
189
|
+
</div>
|
|
190
|
+
|
|
191
|
+
<div class="form-row" style="display:flex; align-items:center;">
|
|
192
|
+
<label for="node-input-name" style="width:180px"><i class="fa fa-tag"></i> <span data-i18n="knxUltimateGarage.node-input-name"></span></label>
|
|
193
|
+
<input type="text" id="node-input-name" style="flex:1">
|
|
194
|
+
</div>
|
|
195
|
+
|
|
196
|
+
<div class="form-row" style="display:flex; align-items:center;">
|
|
197
|
+
<label for="node-input-outputtopic" style="width:180px"><i class="fa fa-comment"></i> <span data-i18n="knxUltimateGarage.node-input-outputtopic"></span></label>
|
|
198
|
+
<input type="text" id="node-input-outputtopic" style="flex:1" data-i18n="[placeholder]knxUltimateGarage.placeholders.outputtopic">
|
|
199
|
+
</div>
|
|
200
|
+
|
|
201
|
+
<hr>
|
|
202
|
+
<div class="form-row" style="margin:4px 0 2px;">
|
|
203
|
+
<span style="font-weight:bold;" data-i18n="knxUltimateGarage.section_commands"></span>
|
|
204
|
+
</div>
|
|
205
|
+
|
|
206
|
+
<div class="form-row" style="display:flex; align-items:center; gap:8px;">
|
|
207
|
+
<label for="node-input-gaCommand" style="width:180px"><i class="fa fa-exchange"></i> <span data-i18n="knxUltimateGarage.command"></span></label>
|
|
208
|
+
<input type="text" id="node-input-gaCommand" style="width:160px" data-i18n="[placeholder]knxUltimateGarage.placeholders.ga">
|
|
209
|
+
<input type="text" id="node-input-nameCommand" style="flex:1" data-i18n="[placeholder]knxUltimateGarage.placeholders.commandName">
|
|
210
|
+
<label for="node-input-dptCommand" style="width:60px; text-align:right">DPT</label>
|
|
211
|
+
<input type="text" id="node-input-dptCommand" style="width:160px" readonly>
|
|
212
|
+
</div>
|
|
213
|
+
|
|
214
|
+
<div class="form-row" style="display:flex; align-items:center; gap:8px;">
|
|
215
|
+
<label for="node-input-gaImpulse" style="width:180px"><i class="fa fa-bolt"></i> <span data-i18n="knxUltimateGarage.impulse"></span></label>
|
|
216
|
+
<input type="text" id="node-input-gaImpulse" style="width:160px" data-i18n="[placeholder]knxUltimateGarage.placeholders.ga">
|
|
217
|
+
<input type="text" id="node-input-nameImpulse" style="flex:1" data-i18n="[placeholder]knxUltimateGarage.placeholders.impulseName">
|
|
218
|
+
<label for="node-input-dptImpulse" style="width:60px; text-align:right">DPT</label>
|
|
219
|
+
<input type="text" id="node-input-dptImpulse" style="width:160px" readonly>
|
|
220
|
+
</div>
|
|
221
|
+
|
|
222
|
+
<div class="form-row" style="display:flex; align-items:center; gap:8px;">
|
|
223
|
+
<label for="node-input-gaMoving" style="width:180px"><i class="fa fa-arrows-h"></i> <span data-i18n="knxUltimateGarage.moving"></span></label>
|
|
224
|
+
<input type="text" id="node-input-gaMoving" style="width:160px" data-i18n="[placeholder]knxUltimateGarage.placeholders.ga">
|
|
225
|
+
<input type="text" id="node-input-nameMoving" style="flex:1" data-i18n="[placeholder]knxUltimateGarage.placeholders.movingName">
|
|
226
|
+
<label for="node-input-dptMoving" style="width:60px; text-align:right">DPT</label>
|
|
227
|
+
<input type="text" id="node-input-dptMoving" style="width:160px" readonly>
|
|
228
|
+
</div>
|
|
229
|
+
|
|
230
|
+
<div class="form-row" style="display:flex; align-items:center; gap:8px;">
|
|
231
|
+
<label for="node-input-gaObstruction" style="width:180px"><i class="fa fa-exclamation-triangle"></i> <span data-i18n="knxUltimateGarage.obstruction"></span></label>
|
|
232
|
+
<input type="text" id="node-input-gaObstruction" style="width:160px" data-i18n="[placeholder]knxUltimateGarage.placeholders.ga">
|
|
233
|
+
<input type="text" id="node-input-nameObstruction" style="flex:1" data-i18n="[placeholder]knxUltimateGarage.placeholders.obstructionName">
|
|
234
|
+
<label for="node-input-dptObstruction" style="width:60px; text-align:right">DPT</label>
|
|
235
|
+
<input type="text" id="node-input-dptObstruction" style="width:160px" readonly>
|
|
236
|
+
</div>
|
|
237
|
+
|
|
238
|
+
<div style="border-top:1px solid #ccc; margin:10px 0 6px;"></div>
|
|
239
|
+
|
|
240
|
+
<div class="form-row" style="margin:4px 0 2px;">
|
|
241
|
+
<span style="font-weight:bold;" data-i18n="knxUltimateGarage.section_inputs"></span>
|
|
242
|
+
</div>
|
|
243
|
+
|
|
244
|
+
<div class="form-row" style="display:flex; align-items:center; gap:8px;">
|
|
245
|
+
<label for="node-input-gaHoldOpen" style="width:180px"><i class="fa fa-pause"></i> <span data-i18n="knxUltimateGarage.holdOpen"></span></label>
|
|
246
|
+
<input type="text" id="node-input-gaHoldOpen" style="width:160px" data-i18n="[placeholder]knxUltimateGarage.placeholders.ga">
|
|
247
|
+
<input type="text" id="node-input-nameHoldOpen" style="flex:1" data-i18n="[placeholder]knxUltimateGarage.placeholders.holdOpenName">
|
|
248
|
+
<label for="node-input-dptHoldOpen" style="width:60px; text-align:right">DPT</label>
|
|
249
|
+
<input type="text" id="node-input-dptHoldOpen" style="width:160px" readonly>
|
|
250
|
+
</div>
|
|
251
|
+
|
|
252
|
+
<div class="form-row" style="display:flex; align-items:center; gap:8px;">
|
|
253
|
+
<label for="node-input-gaDisable" style="width:180px"><i class="fa fa-ban"></i> <span data-i18n="knxUltimateGarage.disable"></span></label>
|
|
254
|
+
<input type="text" id="node-input-gaDisable" style="width:160px" data-i18n="[placeholder]knxUltimateGarage.placeholders.ga">
|
|
255
|
+
<input type="text" id="node-input-nameDisable" style="flex:1" data-i18n="[placeholder]knxUltimateGarage.placeholders.disableName">
|
|
256
|
+
<label for="node-input-dptDisable" style="width:60px; text-align:right">DPT</label>
|
|
257
|
+
<input type="text" id="node-input-dptDisable" style="width:160px" readonly>
|
|
258
|
+
</div>
|
|
259
|
+
|
|
260
|
+
<div class="form-row" style="display:flex; align-items:center; gap:8px;">
|
|
261
|
+
<label for="node-input-gaPhotocell" style="width:180px"><i class="fa fa-lightbulb-o"></i> <span data-i18n="knxUltimateGarage.photocell"></span></label>
|
|
262
|
+
<input type="text" id="node-input-gaPhotocell" style="width:160px" data-i18n="[placeholder]knxUltimateGarage.placeholders.ga">
|
|
263
|
+
<input type="text" id="node-input-namePhotocell" style="flex:1" data-i18n="[placeholder]knxUltimateGarage.placeholders.photocellName">
|
|
264
|
+
<label for="node-input-dptPhotocell" style="width:60px; text-align:right">DPT</label>
|
|
265
|
+
<input type="text" id="node-input-dptPhotocell" style="width:160px" readonly>
|
|
266
|
+
</div>
|
|
267
|
+
|
|
268
|
+
<hr>
|
|
269
|
+
<div class="form-row" style="display:flex; align-items:center;">
|
|
270
|
+
<label for="node-input-autoCloseEnable" style="width:180px"><i class="fa fa-clock-o"></i> <span data-i18n="knxUltimateGarage.autoCloseEnable"></span></label>
|
|
271
|
+
<input type="checkbox" id="node-input-autoCloseEnable" style="width:auto">
|
|
272
|
+
</div>
|
|
273
|
+
|
|
274
|
+
<div class="form-row" style="display:flex; align-items:center;">
|
|
275
|
+
<label for="node-input-autoCloseSeconds" style="width:180px"><i class="fa fa-hourglass-end"></i> <span data-i18n="knxUltimateGarage.autoCloseSeconds"></span></label>
|
|
276
|
+
<input type="number" id="node-input-autoCloseSeconds" style="width:140px" min="1">
|
|
277
|
+
</div>
|
|
278
|
+
|
|
279
|
+
<hr>
|
|
280
|
+
<div class="form-row" style="display:flex; align-items:center;">
|
|
281
|
+
<label for="node-input-emitEvents" style="width:180px"><i class="fa fa-sign-out"></i> <span data-i18n="knxUltimateGarage.node-input-emitEvents"></span></label>
|
|
282
|
+
<input type="checkbox" id="node-input-emitEvents" style="width:auto">
|
|
283
|
+
</div>
|
|
284
|
+
|
|
285
|
+
<br/><br/><br/><br/>
|
|
286
|
+
</script>
|
|
287
|
+
|
|
288
|
+
<script type="text/html" data-help-name="knxUltimateGarage">
|
|
289
|
+
<div data-i18n="knxUltimateGarage.help.intro"></div>
|
|
290
|
+
<h3 data-i18n="knxUltimateGarage.help.commands"></h3>
|
|
291
|
+
<ul>
|
|
292
|
+
<li data-i18n="knxUltimateGarage.help.command_ga"></li>
|
|
293
|
+
<li data-i18n="knxUltimateGarage.help.impulse_ga"></li>
|
|
294
|
+
<li data-i18n="knxUltimateGarage.help.holdopen_ga"></li>
|
|
295
|
+
<li data-i18n="knxUltimateGarage.help.disable_ga"></li>
|
|
296
|
+
</ul>
|
|
297
|
+
<h3 data-i18n="knxUltimateGarage.help.safety"></h3>
|
|
298
|
+
<ul>
|
|
299
|
+
<li data-i18n="knxUltimateGarage.help.photocell_ga"></li>
|
|
300
|
+
<li data-i18n="knxUltimateGarage.help.moving_ga"></li>
|
|
301
|
+
<li data-i18n="knxUltimateGarage.help.obstruction_ga"></li>
|
|
302
|
+
</ul>
|
|
303
|
+
<h3 data-i18n="knxUltimateGarage.help.auto"></h3>
|
|
304
|
+
<ul>
|
|
305
|
+
<li data-i18n="knxUltimateGarage.help.auto_close"></li>
|
|
306
|
+
</ul>
|
|
307
|
+
<h3 data-i18n="knxUltimateGarage.help.events"></h3>
|
|
308
|
+
<p data-i18n="knxUltimateGarage.help.events_desc"></p>
|
|
309
|
+
</script>
|