node-red-contrib-knx-ultimate 4.0.11 → 4.0.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/nodes/commonFunctions.js +7 -1
  3. package/nodes/knxUltimate-config.html +11 -7
  4. package/nodes/knxUltimate-config.js +33 -8
  5. package/nodes/knxUltimate.js +12 -8
  6. package/nodes/knxUltimateAlerter.js +13 -15
  7. package/nodes/knxUltimateAutoResponder.js +7 -7
  8. package/nodes/knxUltimateGlobalContext.js +7 -7
  9. package/nodes/knxUltimateHueBattery.js +9 -13
  10. package/nodes/knxUltimateHueButton.js +7 -7
  11. package/nodes/knxUltimateHueCameraMotion.js +9 -13
  12. package/nodes/knxUltimateHueContactSensor.js +9 -13
  13. package/nodes/knxUltimateHueHumiditySensor.js +9 -13
  14. package/nodes/knxUltimateHueLight.js +48 -14
  15. package/nodes/knxUltimateHueLightSensor.js +9 -13
  16. package/nodes/knxUltimateHueMotion.js +9 -13
  17. package/nodes/knxUltimateHuePlug.js +7 -7
  18. package/nodes/knxUltimateHueScene.js +7 -7
  19. package/nodes/knxUltimateHueTapDial.js +9 -13
  20. package/nodes/knxUltimateHueTemperatureSensor.js +9 -13
  21. package/nodes/knxUltimateHueZigbeeConnectivity.js +9 -13
  22. package/nodes/knxUltimateHuedevice_software_update.js +10 -16
  23. package/nodes/knxUltimateLoadControl.js +7 -7
  24. package/nodes/knxUltimateLogger.js +7 -7
  25. package/nodes/knxUltimateSceneController.js +7 -7
  26. package/nodes/knxUltimateViewer.js +15 -15
  27. package/nodes/knxUltimateWatchDog.js +7 -7
  28. package/nodes/locales/en/knxUltimate-config.html +1 -1
  29. package/nodes/locales/en/knxUltimate-config.json +7 -3
  30. package/nodes/locales/es/knxUltimate-config.html +1 -1
  31. package/nodes/locales/es/knxUltimate-config.json +7 -3
  32. package/nodes/locales/es/knxUltimateLogger.html +1 -1
  33. package/nodes/locales/es/knxUltimateSceneController.html +1 -1
  34. package/nodes/locales/es/knxUltimateWatchDog.html +1 -1
  35. package/nodes/locales/fr/knxUltimate-config.html +1 -1
  36. package/nodes/locales/fr/knxUltimate-config.json +7 -3
  37. package/nodes/locales/it/knxUltimate-config.html +1 -1
  38. package/nodes/locales/it/knxUltimate-config.json +7 -3
  39. package/package.json +2 -2
  40. package/tutorial/knxUltimate-AllNodes-Presentazione.md +190 -0
  41. package/tutorial/knxUltimateHueBattery-teleprompter.txt +5 -1
  42. package/tutorial/knxUltimateHueBattery.md +8 -5
  43. package/tutorial/knxUltimateHueCameraMotion-teleprompter.txt +44 -0
  44. package/tutorial/knxUltimateHueCameraMotion.md +45 -0
  45. package/tutorial/knxUltimateHueHumiditySensor-teleprompter.txt +43 -0
  46. package/tutorial/knxUltimateHueHumiditySensor.md +45 -0
  47. package/tutorial/knxUltimateHueLightSensor-teleprompter.txt +4 -0
  48. package/tutorial/knxUltimateHueLightSensor.md +5 -5
  49. package/tutorial/knxUltimateHueMotion-teleprompter.txt +4 -0
  50. package/tutorial/knxUltimateHueMotion.md +8 -4
  51. package/tutorial/knxUltimateHuePlug-teleprompter.txt +48 -0
  52. package/tutorial/knxUltimateHuePlug.md +49 -0
  53. package/tutorial/knxUltimateHueScene-teleprompter.txt +4 -0
  54. package/tutorial/knxUltimateHueTapDial-teleprompter.txt +4 -0
  55. package/tutorial/knxUltimateHueTapDial.md +8 -4
  56. package/tutorial/knxUltimateHueTemperatureSensor-teleprompter.txt +4 -0
  57. package/tutorial/knxUltimateHueTemperatureSensor.md +5 -4
  58. package/tutorial/knxUltimateHueZigbeeConnectivity-teleprompter.txt +4 -0
  59. package/tutorial/knxUltimateHueZigbeeConnectivity.md +4 -3
  60. package/tutorial/knxUltimateHuedevice_software_update-teleprompter.txt +4 -0
  61. package/tutorial/knxUltimateHuedevice_software_update.md +5 -5
package/CHANGELOG.md CHANGED
@@ -6,6 +6,10 @@
6
6
 
7
7
  # CHANGELOG
8
8
 
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
+ - 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/>
12
+
9
13
  **Version 4.0.11** - October 2025<br/>
10
14
  - 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
15
  - HUE Contact Sensor node: placeholders now leverage i18n translations with graceful fallback when translation keys are missing.<br/>
@@ -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
- await serverId.loadResourcesFromHUEBridge();
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
- statusDisplayPolicy: { value: "all" }
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-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>
1154
1154
  </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>
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 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
+ }
229
254
  };
230
255
  // 24/07/2021 KNX Secure checks...
231
256
  node.keyringFileXML = typeof config.keyringFileXML === "undefined" || config.keyringFileXML.trim() === "" ? "" : config.keyringFileXML;
@@ -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
- 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]' });
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) { 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; }
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
- 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 });
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
- 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]' });
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
- 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 });
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
- 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 });
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 shouldDisplayStatus = (color) => {
55
+ const pushStatus = (status) => {
56
+ if (!status) return;
56
57
  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);
59
62
  }
60
- return true;
61
63
  };
62
64
 
63
65
  const updateStatus = (status) => {
64
66
  if (!status) return;
65
- if (shouldDisplayStatus(status.fill)) {
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
 
@@ -57,19 +57,19 @@ module.exports = function (RED) {
57
57
  node.exposedGAs = []
58
58
  node.timerExposedGAs = null
59
59
 
60
- const shouldDisplayStatus = (color) => {
60
+ const pushStatus = (status) => {
61
+ if (!status) return;
61
62
  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);
64
67
  }
65
- return true;
66
68
  };
67
69
 
68
70
  const updateStatus = (status) => {
69
71
  if (!status) return;
70
- if (shouldDisplayStatus(status.fill)) {
71
- node.status(status);
72
- }
72
+ pushStatus(status);
73
73
  };
74
74
 
75
75
  // Used to call the status update from the config node.
@@ -33,19 +33,19 @@ module.exports = function (RED) {
33
33
  node.outputs = 1;
34
34
  }
35
35
 
36
- const shouldDisplayStatus = (color) => {
36
+ const pushStatus = (status) => {
37
+ if (!status) return;
37
38
  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);
40
43
  }
41
- return true;
42
44
  };
43
45
 
44
46
  const updateStatus = (status) => {
45
47
  if (!status) return;
46
- if (shouldDisplayStatus(status.fill)) {
47
- node.status(status);
48
- }
48
+ pushStatus(status);
49
49
  };
50
50
 
51
51
  const safeSendToKNX = (telegram, context = 'write') => {
@@ -70,9 +70,7 @@ module.exports = function (RED) {
70
70
  const dDate = new Date();
71
71
  payload = typeof payload === "object" ? JSON.stringify(payload) : payload.toString();
72
72
  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 || '') });
76
74
  } catch (error) { }
77
75
  };
78
76
  // Used to call the status update from the HUE config node.
@@ -82,9 +80,7 @@ module.exports = function (RED) {
82
80
  const dDate = new Date();
83
81
  payload = typeof payload === "object" ? JSON.stringify(payload) : payload.toString();
84
82
  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 || '') });
88
84
  } catch (error) { }
89
85
  };
90
86
 
@@ -37,19 +37,19 @@ module.exports = function (RED) {
37
37
  if (node.dimSend === 'down') node.dimSend = { decr_incr: 0, data: 3 };
38
38
  if (node.dimSend === 'stop') node.dimSend = { decr_incr: 0, data: 0 };
39
39
 
40
- const shouldDisplayStatus = (color) => {
40
+ const pushStatus = (status) => {
41
+ if (!status) return;
41
42
  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);
44
47
  }
45
- return true;
46
48
  };
47
49
 
48
50
  const updateStatus = (status) => {
49
51
  if (!status) return;
50
- if (shouldDisplayStatus(status.fill)) {
51
- node.status(status);
52
- }
52
+ pushStatus(status);
53
53
  };
54
54
 
55
55
  const safeSendToKNX = (telegram, context = 'write') => {
@@ -35,19 +35,19 @@ module.exports = function (RED) {
35
35
  node.outputs = 1;
36
36
  }
37
37
 
38
- const shouldDisplayStatus = (color) => {
38
+ const pushStatus = (status) => {
39
+ if (!status) return;
39
40
  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);
42
45
  }
43
- return true;
44
46
  };
45
47
 
46
48
  const updateStatus = (status) => {
47
49
  if (!status) return;
48
- if (shouldDisplayStatus(status.fill)) {
49
- node.status(status);
50
- }
50
+ pushStatus(status);
51
51
  };
52
52
 
53
53
  const safeSendToKNX = (telegram, context = 'write') => {
@@ -69,9 +69,7 @@ module.exports = function (RED) {
69
69
  const dDate = new Date();
70
70
  payload = typeof payload === 'object' ? JSON.stringify(payload) : payload.toString();
71
71
  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 || '') });
75
73
  } catch (error) { /* empty */ }
76
74
  };
77
75
 
@@ -81,9 +79,7 @@ module.exports = function (RED) {
81
79
  const dDate = new Date();
82
80
  payload = typeof payload === 'object' ? JSON.stringify(payload) : payload.toString();
83
81
  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 || '') });
87
83
  } catch (error) { /* empty */ }
88
84
  };
89
85
 
@@ -25,19 +25,19 @@ module.exports = function (RED) {
25
25
  node.hueDevice = config.hueDevice
26
26
  node.initializingAtStart = false
27
27
 
28
- const shouldDisplayStatus = (color) => {
28
+ const pushStatus = (status) => {
29
+ if (!status) return;
29
30
  const provider = node.serverKNX;
30
- if (provider && typeof provider.shouldDisplayStatus === 'function') {
31
- return provider.shouldDisplayStatus(color);
31
+ if (provider && typeof provider.applyStatusUpdate === 'function') {
32
+ provider.applyStatusUpdate(node, status);
33
+ } else {
34
+ node.status(status);
32
35
  }
33
- return true;
34
36
  };
35
37
 
36
38
  const updateStatus = (status) => {
37
39
  if (!status) return;
38
- if (shouldDisplayStatus(status.fill)) {
39
- node.status(status);
40
- }
40
+ pushStatus(status);
41
41
  };
42
42
 
43
43
  const safeSendToKNX = (telegram, context = 'write') => {
@@ -62,9 +62,7 @@ module.exports = function (RED) {
62
62
  const dDate = new Date();
63
63
  payload = typeof payload === "object" ? JSON.stringify(payload) : payload.toString();
64
64
  node.sKNXNodeStatusText = `|KNX: ${text} ${payload} (${dDate.getDate()}, ${dDate.toLocaleTimeString()})`;
65
- if (shouldDisplayStatus(fill)) {
66
- node.status({ fill, shape, text: (node.sHUENodeStatusText || '') + ' ' + (node.sKNXNodeStatusText || '') });
67
- }
65
+ pushStatus({ fill, shape, text: (node.sHUENodeStatusText || '') + ' ' + (node.sKNXNodeStatusText || '') });
68
66
  } catch (error) { }
69
67
  };
70
68
  // Used to call the status update from the HUE config node.
@@ -74,9 +72,7 @@ module.exports = function (RED) {
74
72
  const dDate = new Date();
75
73
  payload = typeof payload === "object" ? JSON.stringify(payload) : payload.toString();
76
74
  node.sHUENodeStatusText = `|HUE: ${text} ${payload} (${dDate.getDate()}, ${dDate.toLocaleTimeString()})`;
77
- if (shouldDisplayStatus(fill)) {
78
- node.status({ fill, shape, text: node.sHUENodeStatusText + ' ' + (node.sKNXNodeStatusText || '') });
79
- }
75
+ pushStatus({ fill, shape, text: node.sHUENodeStatusText + ' ' + (node.sKNXNodeStatusText || '') });
80
76
  } catch (error) { }
81
77
  };
82
78
 
@@ -30,19 +30,19 @@ module.exports = function (RED) {
30
30
  node.enableNodePINS = (config.enableNodePINS === undefined || config.enableNodePINS === 'yes');
31
31
  node.outputs = node.enableNodePINS ? 1 : 0;
32
32
 
33
- const shouldDisplayStatus = (color) => {
33
+ const pushStatus = (status) => {
34
+ if (!status) return;
34
35
  const provider = node.serverKNX;
35
- if (provider && typeof provider.shouldDisplayStatus === 'function') {
36
- return provider.shouldDisplayStatus(color);
36
+ if (provider && typeof provider.applyStatusUpdate === 'function') {
37
+ provider.applyStatusUpdate(node, status);
38
+ } else {
39
+ node.status(status);
37
40
  }
38
- return true;
39
41
  };
40
42
 
41
43
  const updateStatus = (status) => {
42
44
  if (!status) return;
43
- if (shouldDisplayStatus(status.fill)) {
44
- node.status(status);
45
- }
45
+ pushStatus(status);
46
46
  };
47
47
 
48
48
  const safeSendToKNX = (telegram, context = 'write') => {
@@ -64,9 +64,7 @@ module.exports = function (RED) {
64
64
  const dDate = new Date();
65
65
  payload = typeof payload === 'object' ? JSON.stringify(payload) : payload.toString();
66
66
  node.sKNXNodeStatusText = `|KNX: ${text} ${payload} (${dDate.getDate()}, ${dDate.toLocaleTimeString()})`;
67
- if (shouldDisplayStatus(fill)) {
68
- node.status({ fill, shape, text: (node.sHUENodeStatusText || '') + ' ' + (node.sKNXNodeStatusText || '') });
69
- }
67
+ pushStatus({ fill, shape, text: (node.sHUENodeStatusText || '') + ' ' + (node.sKNXNodeStatusText || '') });
70
68
  } catch (error) { /* empty */ }
71
69
  };
72
70
 
@@ -76,9 +74,7 @@ module.exports = function (RED) {
76
74
  const dDate = new Date();
77
75
  payload = typeof payload === 'object' ? JSON.stringify(payload) : payload.toString();
78
76
  node.sHUENodeStatusText = `|HUE: ${text} ${payload} (${dDate.getDate()}, ${dDate.toLocaleTimeString()})`;
79
- if (shouldDisplayStatus(fill)) {
80
- node.status({ fill, shape, text: node.sHUENodeStatusText + ' ' + (node.sKNXNodeStatusText || '') });
81
- }
77
+ pushStatus({ fill, shape, text: node.sHUENodeStatusText + ' ' + (node.sKNXNodeStatusText || '') });
82
78
  } catch (error) { /* empty */ }
83
79
  };
84
80
 
@@ -226,6 +226,7 @@ module.exports = function (RED) {
226
226
  node.formatnegativevalue = "leave";
227
227
  node.formatdecimalsvalue = 2;
228
228
  node.currentHUEDevice = undefined; // At start, this value is filled by a call to HUE api. It stores a value representing the current light status.
229
+ node.lastKnownBrightness = undefined; // Stores the latest non-zero brightness to honour "keep brightness" behaviour when toggling via KNX.
229
230
  node.HUEDeviceWhileDaytime = null;// This retains the HUE device status while daytime, to be restored after nighttime elapsed.
230
231
  node.HUELightsBelongingToGroupWhileDaytime = null; // Array contains all light belonging to the grouped_light (if grouped_light is selected)
231
232
  node.DayTime = true;
@@ -235,19 +236,19 @@ module.exports = function (RED) {
235
236
  node.timerCheckForFastLightSwitch = null;
236
237
  node.HSVObject = null; //{ h, s, v };// Store the current light calculated HSV
237
238
 
238
- const shouldDisplayStatus = (color) => {
239
+ const pushStatus = (status) => {
240
+ if (!status) return;
239
241
  const provider = node.serverKNX;
240
- if (provider && typeof provider.shouldDisplayStatus === 'function') {
241
- return provider.shouldDisplayStatus(color);
242
+ if (provider && typeof provider.applyStatusUpdate === 'function') {
243
+ provider.applyStatusUpdate(node, status);
244
+ } else {
245
+ node.status(status);
242
246
  }
243
- return true;
244
247
  };
245
248
 
246
249
  const updateStatus = (status) => {
247
250
  if (!status) return;
248
- if (shouldDisplayStatus(status.fill)) {
249
- node.status(status);
250
- }
251
+ pushStatus(status);
251
252
  };
252
253
 
253
254
  const safeSendToKNX = (telegram, context = 'write') => {
@@ -307,6 +308,9 @@ module.exports = function (RED) {
307
308
  const handleLightSwitch = (msg) => {
308
309
  let state = {};
309
310
  msg.payload = dptlib.fromBuffer(msg.knx.rawValue, dptlib.resolve(config.dptLightSwitch));
311
+ if (node.lastKnownBrightness === undefined && node.currentHUEDevice?.dimming?.brightness > 0) {
312
+ node.lastKnownBrightness = node.currentHUEDevice.dimming.brightness;
313
+ }
310
314
 
311
315
  if (config.restoreDayMode === "setDayByFastSwitchLightSingle" || config.restoreDayMode === "setDayByFastSwitchLightALL") {
312
316
  if (node.DayTime === false) {
@@ -343,6 +347,7 @@ module.exports = function (RED) {
343
347
  state = { on: { on: true }, dimming: node.HUEDeviceWhileDaytime.dimming, color: node.HUEDeviceWhileDaytime.color, color_temperature: node.HUEDeviceWhileDaytime.color_temperature };
344
348
  if (node.HUEDeviceWhileDaytime.color_temperature !== undefined && node.HUEDeviceWhileDaytime.color_temperature.mirek === null) delete state.color_temperature;
345
349
  queueHueCommand(state);
350
+ if (state.dimming?.brightness > 0) node.lastKnownBrightness = state.dimming.brightness;
346
351
  reportHueStatus("Restore light status");
347
352
  node.HUEDeviceWhileDaytime = null;
348
353
  } else if (node.isGrouped_light === true && node.HUELightsBelongingToGroupWhileDaytime !== null) {
@@ -357,13 +362,14 @@ module.exports = function (RED) {
357
362
  for (let index = 0; index < node.HUELightsBelongingToGroupWhileDaytime.length; index++) {
358
363
  const element = node.HUELightsBelongingToGroupWhileDaytime[index].light[0];
359
364
  if (bAtLeastOneIsOn === true) {
360
- state = { on: element.on, dimming: element.dimming, color: element.color, color_temperature: element.color_temperature };
361
- } else {
362
- state = { on: { on: true }, dimming: element.dimming, color: element.color, color_temperature: element.color_temperature };
363
- }
364
- if (element.color_temperature !== undefined && element.color_temperature.mirek === null) delete state.color_temperature;
365
- node.serverHue.hueManager.writeHueQueueAdd(element.id, state, "setLight");
365
+ state = { on: element.on, dimming: element.dimming, color: element.color, color_temperature: element.color_temperature };
366
+ } else {
367
+ state = { on: { on: true }, dimming: element.dimming, color: element.color, color_temperature: element.color_temperature };
366
368
  }
369
+ if (element.color_temperature !== undefined && element.color_temperature.mirek === null) delete state.color_temperature;
370
+ node.serverHue.hueManager.writeHueQueueAdd(element.id, state, "setLight");
371
+ if (state.dimming?.brightness > 0) node.lastKnownBrightness = state.dimming.brightness;
372
+ }
367
373
  reportHueStatus("Resuming all group's light");
368
374
  node.HUELightsBelongingToGroupWhileDaytime = null;
369
375
  return;
@@ -410,6 +416,7 @@ module.exports = function (RED) {
410
416
  delete state.color_temperature;
411
417
  }
412
418
  queueHueCommand(state);
419
+ if (typeof dbright === "number" && dbright > 0) node.lastKnownBrightness = dbright;
413
420
  reportHueStatus(JSON.stringify(msg.payload));
414
421
  return;
415
422
  }
@@ -429,12 +436,30 @@ module.exports = function (RED) {
429
436
  delete state.color_temperature;
430
437
  }
431
438
  queueHueCommand(state);
439
+ if (typeof bBright === "number" && bBright > 0) node.lastKnownBrightness = bBright;
432
440
  reportHueStatus(JSON.stringify(msg.payload));
433
441
  return;
434
442
  }
435
443
  if (node.currentHUEDevice.dimming !== undefined) {
436
- state = { dimming: { brightness: brightnessChoosen || 100 }, on: { on: true } };
444
+ let targetBrightness;
445
+ if (brightnessChoosen !== undefined && brightnessChoosen !== null && brightnessChoosen !== "") {
446
+ const parsed = Number(brightnessChoosen);
447
+ if (!Number.isNaN(parsed)) targetBrightness = parsed;
448
+ }
449
+ if (targetBrightness === undefined) {
450
+ if (typeof node.lastKnownBrightness === "number" && node.lastKnownBrightness > 0) {
451
+ targetBrightness = node.lastKnownBrightness;
452
+ } else if (typeof node.currentHUEDevice.dimming.brightness === "number" && node.currentHUEDevice.dimming.brightness > 0) {
453
+ targetBrightness = node.currentHUEDevice.dimming.brightness;
454
+ }
455
+ }
456
+ if (targetBrightness === undefined || targetBrightness <= 0) {
457
+ targetBrightness = 100;
458
+ }
459
+ state = { dimming: { brightness: targetBrightness }, on: { on: true } };
460
+ if (targetBrightness > 0) node.lastKnownBrightness = targetBrightness;
437
461
  queueHueCommand(state);
462
+ if (typeof targetBrightness === "number" && targetBrightness > 0) node.lastKnownBrightness = targetBrightness;
438
463
  reportHueStatus(JSON.stringify(msg.payload));
439
464
  return;
440
465
  }
@@ -443,6 +468,9 @@ module.exports = function (RED) {
443
468
  reportHueStatus(JSON.stringify(msg.payload));
444
469
  }
445
470
  } else {
471
+ if (node.currentHUEDevice?.dimming?.brightness > 0) {
472
+ node.lastKnownBrightness = node.currentHUEDevice.dimming.brightness;
473
+ }
446
474
  state = { on: { on: false } };
447
475
  queueHueCommand(state);
448
476
  reportHueStatus(JSON.stringify(msg.payload));
@@ -584,6 +612,7 @@ module.exports = function (RED) {
584
612
  case config.GALightBrightness:
585
613
  msg.payload = dptlib.fromBuffer(msg.knx.rawValue, dptlib.resolve(config.dptLightBrightness));
586
614
  state = { dimming: { brightness: msg.payload } };
615
+ if (msg.payload > 0) node.lastKnownBrightness = msg.payload;
587
616
  if (node.currentHUEDevice === undefined) {
588
617
  // Grouped light
589
618
  state.on = { on: msg.payload > 0 };
@@ -1332,6 +1361,11 @@ module.exports = function (RED) {
1332
1361
  } else {
1333
1362
  if (node.currentHUEDevice.on !== undefined && node.currentHUEDevice.on.on === false && (receivedHUEObject.on === undefined || (receivedHUEObject.on !== undefined && receivedHUEObject.on.on === true))) node.updateKNXLightState(receivedHUEObject.dimming.brightness > 0);
1334
1363
  node.updateKNXBrightnessState(receivedHUEObject.dimming.brightness);
1364
+ if (receivedHUEObject.dimming.brightness > 0) {
1365
+ node.lastKnownBrightness = receivedHUEObject.dimming.brightness;
1366
+ } else if (node.currentHUEDevice?.dimming?.brightness > 0) {
1367
+ node.lastKnownBrightness = node.currentHUEDevice.dimming.brightness;
1368
+ }
1335
1369
  // If the brightness reaches zero, the hue lamp "on" property must be set to zero as well
1336
1370
  if (receivedHUEObject.dimming.brightness === 0 && node.currentHUEDevice.on !== undefined && node.currentHUEDevice.on.on === true) {
1337
1371
  node.serverHue.hueManager.writeHueQueueAdd(