Skip to content

Commit 051886f

Browse files
committed
.
1 parent 54e5128 commit 051886f

35 files changed

+282
-274
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66

77
# CHANGELOG
88

9+
**Version 4.0.12** - October 2025<br/>
10+
- 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/>
11+
912
**Version 4.0.11** - October 2025<br/>
1013
- 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/>
1114
- HUE Contact Sensor node: placeholders now leverage i18n translations with graceful fallback when translation keys are missing.<br/>

nodes/knxUltimate-config.html

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
tunnelUserPassword: { value: "" },
2929
tunnelUserId: { value: "" },
3030
autoReconnect: { value: "yes" },
31-
statusDisplayPolicy: { value: "all" }
31+
statusUpdateThrottle: { value: "0" }
3232
},
3333
credentials: {
3434
keyringFilePassword: { type: "password" }
@@ -1148,13 +1148,17 @@
11481148
</div>
11491149

11501150
<div class="form-row">
1151-
<label for="node-config-input-statusDisplayPolicy">
1152-
<i class="fa fa-eye"></i>
1153-
<span data-i18n="knxUltimate-config.advanced.status_display_policy"></span>
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>
11541154
</label>
1155-
<select id="node-config-input-statusDisplayPolicy" style="width:40%;">
1156-
<option value="all" data-i18n="knxUltimate-config.advanced.status_display_all"></option>
1157-
<option value="errors" data-i18n="knxUltimate-config.advanced.status_display_errors"></option>
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>
11581162
</select>
11591163
</div>
11601164
</p>

nodes/knxUltimate-config.js

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,6 @@ const loggerClass = require('./utils/sysLogger')
2424
const payloadRounder = require("./utils/payloadManipulation");
2525
const utils = require('./utils/utils');
2626

27-
const STATUS_DISPLAY_ALLOWED_COLORS = new Set(['red', 'yellow']);
28-
2927
// DATAPONT MANIPULATION HELPERS
3028
// ####################
3129
const sortBy = (field) => (a, b) => {
@@ -220,12 +218,39 @@ module.exports = (RED) => {
220218
node.autoReconnect = true;
221219
}
222220
node.ignoreTelegramsWithRepeatedFlag = config.ignoreTelegramsWithRepeatedFlag === undefined ? false : config.ignoreTelegramsWithRepeatedFlag;
223-
const policyFromConfig = typeof config.statusDisplayPolicy === "string" ? config.statusDisplayPolicy : "all";
224-
node.statusDisplayPolicy = ['all', 'errors'].includes(policyFromConfig) ? policyFromConfig : "all";
225-
node.shouldDisplayStatus = (fill) => {
226-
if (node.statusDisplayPolicy !== 'errors') return true;
227-
const normalizedFill = (typeof fill === 'string' ? fill : '').toLowerCase();
228-
return STATUS_DISPLAY_ALLOWED_COLORS.has(normalizedFill);
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+
}
229254
};
230255
// 24/07/2021 KNX Secure checks...
231256
node.keyringFileXML = typeof config.keyringFileXML === "undefined" || config.keyringFileXML.trim() === "" ? "" : config.keyringFileXML;

nodes/knxUltimate.js

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,17 @@ module.exports = function (RED) {
1111
RED.nodes.createNode(this, config);
1212
const node = this;
1313
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+
1423
if (node.serverKNX === undefined) {
15-
node.status({ fill: 'red', shape: 'dot', text: '[THE GATEWAY NODE HAS BEEN DISABLED]' });
24+
pushStatus({ fill: 'red', shape: 'dot', text: '[THE GATEWAY NODE HAS BEEN DISABLED]' });
1625
return;
1726
}
1827

@@ -22,7 +31,7 @@ module.exports = function (RED) {
2231
fill, shape, text, payload, GA, dpt, devicename,
2332
}) => {
2433
try {
25-
if (node.serverKNX === null) { node.status({ fill: 'red', shape: 'dot', text: '[NO GATEWAY SELECTED]' }); return; }
34+
if (node.serverKNX === null) { pushStatus({ fill: 'red', shape: 'dot', text: '[NO GATEWAY SELECTED]' }); return; }
2635
if (node.icountMessageInWindow == -999) return; // Locked out, doesn't change status.
2736
const dDate = new Date();
2837
// 30/08/2019 Display only the things selected in the config
@@ -31,12 +40,7 @@ module.exports = function (RED) {
3140
dpt = (typeof dpt === 'undefined' || dpt == '') ? '' : ` DPT${dpt}`;
3241
payload = typeof payload === 'object' ? JSON.stringify(payload) : payload;
3342
const statusText = `${GA + payload + (node.listenallga === true ? ` ${devicename}` : '')} (day ${dDate.getDate()}, ${dDate.toLocaleTimeString()}) ${text}`;
34-
const shouldUpdateStatus = (node.serverKNX && typeof node.serverKNX.shouldDisplayStatus === 'function')
35-
? node.serverKNX.shouldDisplayStatus(fill)
36-
: true;
37-
if (shouldUpdateStatus) {
38-
node.status({ fill, shape, text: statusText });
39-
}
43+
pushStatus({ fill, shape, text: statusText });
4044
// 16/02/2020 signal errors to the server
4145
if (fill.toUpperCase() === 'RED') {
4246
if (node.serverKNX) {

nodes/knxUltimateAlerter.js

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,18 @@ module.exports = function (RED) {
1111
RED.nodes.createNode(this, config);
1212
const node = this;
1313
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+
1424
if (node.serverKNX === undefined) {
15-
node.status({ fill: 'red', shape: 'dot', text: '[THE GATEWAY NODE HAS BEEN DISABLED]' });
25+
pushStatus({ fill: 'red', shape: 'dot', text: '[THE GATEWAY NODE HAS BEEN DISABLED]' });
1626
return;
1727
}
1828
node.name = config.name || 'KNX Alerter';
@@ -33,14 +43,6 @@ module.exports = function (RED) {
3343
node.whentostart = config.whentostart === undefined ? 'ifnewalert' : config.whentostart;
3444
node.timerinterval = (config.timerinterval === undefined || config.timerinterval == '') ? '2' : config.timerinterval;
3545

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-
4446
if (config.initialreadGAInRules === undefined) {
4547
node.initialread = 1;
4648
} else {
@@ -61,9 +63,7 @@ module.exports = function (RED) {
6163
devicename = devicename || '';
6264
dpt = (typeof dpt === 'undefined' || dpt == '') ? '' : ' DPT' + dpt;
6365
payload = typeof payload === 'object' ? JSON.stringify(payload) : payload;
64-
if (shouldDisplayStatus(fill)) {
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 });
6767
} catch (error) {
6868
}
6969
};
@@ -76,9 +76,7 @@ module.exports = function (RED) {
7676
devicename = devicename || '';
7777
dpt = (typeof dpt === 'undefined' || dpt == '') ? '' : ' DPT' + dpt;
7878
try {
79-
if (shouldDisplayStatus(fill)) {
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 });
8280
} catch (error) {
8381
}
8482
};

nodes/knxUltimateAutoResponder.js

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -52,19 +52,19 @@ module.exports = function (RED) {
5252
node.commandText = []; // Raw list Respond To
5353
node.timerSaveExposedGAs = null;
5454

55-
const shouldDisplayStatus = (color) => {
55+
const pushStatus = (status) => {
56+
if (!status) return;
5657
const provider = node.serverKNX;
57-
if (provider && typeof provider.shouldDisplayStatus === 'function') {
58-
return provider.shouldDisplayStatus(color);
58+
if (provider && typeof provider.applyStatusUpdate === 'function') {
59+
provider.applyStatusUpdate(node, status);
60+
} else {
61+
node.status(status);
5962
}
60-
return true;
6163
};
6264

6365
const updateStatus = (status) => {
6466
if (!status) return;
65-
if (shouldDisplayStatus(status.fill)) {
66-
node.status(status);
67-
}
67+
pushStatus(status);
6868
};
6969
if (node.serverKNX === null) { updateStatus({ fill: 'red', shape: 'dot', text: '[NO GATEWAY SELECTED]' }); return; }
7070

nodes/knxUltimateGlobalContext.js

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -57,19 +57,19 @@ module.exports = function (RED) {
5757
node.exposedGAs = []
5858
node.timerExposedGAs = null
5959

60-
const shouldDisplayStatus = (color) => {
60+
const pushStatus = (status) => {
61+
if (!status) return;
6162
const provider = node.serverKNX;
62-
if (provider && typeof provider.shouldDisplayStatus === 'function') {
63-
return provider.shouldDisplayStatus(color);
63+
if (provider && typeof provider.applyStatusUpdate === 'function') {
64+
provider.applyStatusUpdate(node, status);
65+
} else {
66+
node.status(status);
6467
}
65-
return true;
6668
};
6769

6870
const updateStatus = (status) => {
6971
if (!status) return;
70-
if (shouldDisplayStatus(status.fill)) {
71-
node.status(status);
72-
}
72+
pushStatus(status);
7373
};
7474

7575
// Used to call the status update from the config node.

nodes/knxUltimateHueBattery.js

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -33,19 +33,19 @@ module.exports = function (RED) {
3333
node.outputs = 1;
3434
}
3535

36-
const shouldDisplayStatus = (color) => {
36+
const pushStatus = (status) => {
37+
if (!status) return;
3738
const provider = node.serverKNX;
38-
if (provider && typeof provider.shouldDisplayStatus === 'function') {
39-
return provider.shouldDisplayStatus(color);
39+
if (provider && typeof provider.applyStatusUpdate === 'function') {
40+
provider.applyStatusUpdate(node, status);
41+
} else {
42+
node.status(status);
4043
}
41-
return true;
4244
};
4345

4446
const updateStatus = (status) => {
4547
if (!status) return;
46-
if (shouldDisplayStatus(status.fill)) {
47-
node.status(status);
48-
}
48+
pushStatus(status);
4949
};
5050

5151
const safeSendToKNX = (telegram, context = 'write') => {
@@ -70,9 +70,7 @@ module.exports = function (RED) {
7070
const dDate = new Date();
7171
payload = typeof payload === "object" ? JSON.stringify(payload) : payload.toString();
7272
node.sKNXNodeStatusText = `|KNX: ${text} ${payload} (${dDate.getDate()}, ${dDate.toLocaleTimeString()})`;
73-
if (shouldDisplayStatus(fill)) {
74-
node.status({ fill, shape, text: (node.sHUENodeStatusText || '') + ' ' + (node.sKNXNodeStatusText || '') });
75-
}
73+
pushStatus({ fill, shape, text: (node.sHUENodeStatusText || '') + ' ' + (node.sKNXNodeStatusText || '') });
7674
} catch (error) { }
7775
};
7876
// Used to call the status update from the HUE config node.
@@ -82,9 +80,7 @@ module.exports = function (RED) {
8280
const dDate = new Date();
8381
payload = typeof payload === "object" ? JSON.stringify(payload) : payload.toString();
8482
node.sHUENodeStatusText = `|HUE: ${text} ${payload} (${dDate.getDate()}, ${dDate.toLocaleTimeString()})`;
85-
if (shouldDisplayStatus(fill)) {
86-
node.status({ fill, shape, text: node.sHUENodeStatusText + ' ' + (node.sKNXNodeStatusText || '') });
87-
}
83+
pushStatus({ fill, shape, text: node.sHUENodeStatusText + ' ' + (node.sKNXNodeStatusText || '') });
8884
} catch (error) { }
8985
};
9086

nodes/knxUltimateHueButton.js

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -37,19 +37,19 @@ module.exports = function (RED) {
3737
if (node.dimSend === 'down') node.dimSend = { decr_incr: 0, data: 3 };
3838
if (node.dimSend === 'stop') node.dimSend = { decr_incr: 0, data: 0 };
3939

40-
const shouldDisplayStatus = (color) => {
40+
const pushStatus = (status) => {
41+
if (!status) return;
4142
const provider = node.serverKNX;
42-
if (provider && typeof provider.shouldDisplayStatus === 'function') {
43-
return provider.shouldDisplayStatus(color);
43+
if (provider && typeof provider.applyStatusUpdate === 'function') {
44+
provider.applyStatusUpdate(node, status);
45+
} else {
46+
node.status(status);
4447
}
45-
return true;
4648
};
4749

4850
const updateStatus = (status) => {
4951
if (!status) return;
50-
if (shouldDisplayStatus(status.fill)) {
51-
node.status(status);
52-
}
52+
pushStatus(status);
5353
};
5454

5555
const safeSendToKNX = (telegram, context = 'write') => {

nodes/knxUltimateHueCameraMotion.js

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -35,19 +35,19 @@ module.exports = function (RED) {
3535
node.outputs = 1;
3636
}
3737

38-
const shouldDisplayStatus = (color) => {
38+
const pushStatus = (status) => {
39+
if (!status) return;
3940
const provider = node.serverKNX;
40-
if (provider && typeof provider.shouldDisplayStatus === 'function') {
41-
return provider.shouldDisplayStatus(color);
41+
if (provider && typeof provider.applyStatusUpdate === 'function') {
42+
provider.applyStatusUpdate(node, status);
43+
} else {
44+
node.status(status);
4245
}
43-
return true;
4446
};
4547

4648
const updateStatus = (status) => {
4749
if (!status) return;
48-
if (shouldDisplayStatus(status.fill)) {
49-
node.status(status);
50-
}
50+
pushStatus(status);
5151
};
5252

5353
const safeSendToKNX = (telegram, context = 'write') => {
@@ -69,9 +69,7 @@ module.exports = function (RED) {
6969
const dDate = new Date();
7070
payload = typeof payload === 'object' ? JSON.stringify(payload) : payload.toString();
7171
node.sKNXNodeStatusText = `|KNX: ${text} ${payload} (${dDate.getDate()}, ${dDate.toLocaleTimeString()})`;
72-
if (shouldDisplayStatus(fill)) {
73-
node.status({ fill, shape, text: (node.sHUENodeStatusText || '') + ' ' + (node.sKNXNodeStatusText || '') });
74-
}
72+
pushStatus({ fill, shape, text: (node.sHUENodeStatusText || '') + ' ' + (node.sKNXNodeStatusText || '') });
7573
} catch (error) { /* empty */ }
7674
};
7775

@@ -81,9 +79,7 @@ module.exports = function (RED) {
8179
const dDate = new Date();
8280
payload = typeof payload === 'object' ? JSON.stringify(payload) : payload.toString();
8381
node.sHUENodeStatusText = `|HUE: ${text} ${payload} (${dDate.getDate()}, ${dDate.toLocaleTimeString()})`;
84-
if (shouldDisplayStatus(fill)) {
85-
node.status({ fill, shape, text: node.sHUENodeStatusText + ' ' + (node.sKNXNodeStatusText || '') });
86-
}
82+
pushStatus({ fill, shape, text: node.sHUENodeStatusText + ' ' + (node.sKNXNodeStatusText || '') });
8783
} catch (error) { /* empty */ }
8884
};
8985

0 commit comments

Comments
 (0)