node-red-contrib-knx-ultimate 8.1.1 → 8.3.0

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 (45) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/nodes/commonFunctions.js +13 -151
  3. package/nodes/knxUltimate-config.html +96 -158
  4. package/nodes/knxUltimate-config.js +41 -51
  5. package/nodes/knxUltimate.html +4 -0
  6. package/nodes/knxUltimateUtility.html +7 -0
  7. package/nodes/knxUltimateViewer.html +3 -0
  8. package/nodes/knxUltimateViewer.js +3 -1
  9. package/nodes/locales/de/knxUltimate-config.html +8 -5
  10. package/nodes/locales/de/knxUltimate-config.json +22 -20
  11. package/nodes/locales/de/knxUltimate.html +2 -0
  12. package/nodes/locales/de/knxUltimateUtility.html +6 -0
  13. package/nodes/locales/de/knxUltimateViewer.html +2 -0
  14. package/nodes/locales/en/knxUltimate-config.html +8 -5
  15. package/nodes/locales/en/knxUltimate-config.json +22 -20
  16. package/nodes/locales/en/knxUltimate.html +2 -0
  17. package/nodes/locales/en/knxUltimateUtility.html +6 -0
  18. package/nodes/locales/en/knxUltimateViewer.html +2 -0
  19. package/nodes/locales/es/knxUltimate-config.html +12 -6
  20. package/nodes/locales/es/knxUltimate-config.json +22 -20
  21. package/nodes/locales/es/knxUltimate.html +2 -0
  22. package/nodes/locales/es/knxUltimateUtility.html +6 -0
  23. package/nodes/locales/es/knxUltimateViewer.html +2 -0
  24. package/nodes/locales/fr/knxUltimate-config.html +10 -7
  25. package/nodes/locales/fr/knxUltimate-config.json +22 -20
  26. package/nodes/locales/fr/knxUltimate.html +2 -0
  27. package/nodes/locales/fr/knxUltimateUtility.html +6 -0
  28. package/nodes/locales/fr/knxUltimateViewer.html +2 -0
  29. package/nodes/locales/it/knxUltimate-config.html +8 -5
  30. package/nodes/locales/it/knxUltimate-config.json +22 -20
  31. package/nodes/locales/it/knxUltimate.html +2 -0
  32. package/nodes/locales/it/knxUltimateUtility.html +6 -0
  33. package/nodes/locales/it/knxUltimateViewer.html +2 -0
  34. package/nodes/locales/zh-CN/knxUltimate-config.html +8 -5
  35. package/nodes/locales/zh-CN/knxUltimate-config.json +22 -20
  36. package/nodes/locales/zh-CN/knxUltimate.html +2 -0
  37. package/nodes/locales/zh-CN/knxUltimateUtility.html +6 -0
  38. package/nodes/locales/zh-CN/knxUltimateViewer.html +2 -0
  39. package/nodes/utils/fileStorage.js +376 -0
  40. package/nodes/utils/knxUtilityProfiles/runtime/autoresponder.js +15 -19
  41. package/nodes/utils/knxUtilityProfiles/runtime/logger.js +7 -4
  42. package/nodes/utils/knxUtilityProfiles/runtime/scenecontroller.js +12 -44
  43. package/nodes/utils/knxViewerHistory.js +49 -77
  44. package/package.json +2 -2
  45. package/resources/htmlUtils.js +24 -2
package/CHANGELOG.md CHANGED
@@ -6,6 +6,19 @@
6
6
 
7
7
  # CHANGELOG
8
8
 
9
+ **Version 8.3.0** - October 2026
10
+
11
+ - Updated the KNX engine to `knxultimate` 6.3.0.
12
+ - New **File Cache Control** tab in the KNX gateway configuration, to reduce the wear of SD cards ([#536](https://github.com/Supergiovane/node-red-contrib-knx-ultimate/issues/536)). The **Disk-wear management** option sets when every file written by KNX Ultimate (GA values, Scene Controller scenes, AutoResponder values, Logger file, Viewer history) reaches the disk: **Freshness priority** (every 5 seconds, default), **Balanced** (when the gateway connects or disconnects), **Custom interval** (from 5 seconds to 30 days) or **Disable writing** (data kept in memory only, lost at every Node-RED restart). The log is not affected.
13
+ - Files are now written only when their content changes: the GA values file is no longer rewritten every 5 seconds when nothing has changed.
14
+ - When writing is disabled, the nodes that use files show a reminder in their editor.
15
+ - New **Wait for the tunnelling ACK (Tunnel UDP)** option in the **Advanced** tab of the KNX gateway configuration. **Yes** waits for the KNX/IP interface to confirm every telegram, sends an unconfirmed telegram once more and then reconnects, also when the bus ACK request is suppressed: select it together with **Suppress the bus ACK request** for eibPORT and Loxone gateways. **Auto** (default) keeps the previous behaviour.
16
+ - **Suppress ACK request in tunneling mode** is now **Suppress the bus ACK request**, with a clearer description in the editor and in the documentation: it also applies to Multicast and Serial FT1.2.
17
+ - The log level moved from the **Advanced** tab to the new **File Cache Control** tab.
18
+ - Removed the **Utility** tab of the gateway configuration.
19
+ - The Viewer no longer rewrites its oldest history file every minute: a whole hour of history is deleted once it has fully expired.
20
+ - Removed 145 duplicated wiki pages with a double language prefix.
21
+
9
22
  **Version 8.1.1** - October 2026
10
23
 
11
24
  - Updated the KNX engine to `knxultimate` 6.2.1.
@@ -1,11 +1,11 @@
1
1
  // Utility function
2
2
  // until node-red 3.1.0, there is a bug creating a plugin, so for backward compatibility, i must use a JS as a node.
3
3
  const oOS = require('os')
4
- const fs = require('fs')
5
4
  const path = require('path')
6
5
  const dptlib = require('knxultimate').dptlib
7
6
  const KNXClient = require('knxultimate').KNXClient
8
7
  const { normalizeAuthFromAccessTokenQuery } = require('./utils/httpAdminAccessToken')
8
+ const { storageOf } = require('./utils/fileStorage')
9
9
 
10
10
  // DATAPONT MANIPULATION HELPERS
11
11
  // ####################
@@ -66,17 +66,6 @@ module.exports = (RED) => {
66
66
 
67
67
  // }
68
68
 
69
- // 11/03/2020 Delete scene saved file, from html
70
- RED.httpAdmin.get('/knxultimatescenecontrollerdelete'), (req, res) => {
71
- // Delete the file
72
- try {
73
- const serverId = RED.nodes.getNode(req.query.serverId) // Retrieve node.id of the config node.
74
- const newPath = `${serverId.userDir}/scenecontroller/SceneController_${req.query.FileName}`
75
- fs.unlinkSync(newPath)
76
- } catch (error) { if (node.sysLogger !== undefined && node.sysLogger !== null) node.sysLogger.warn(`e ${error}`) }
77
- res.json({ status: 220 })
78
- }
79
-
80
69
  // Endpoint for reading csv/esf by the other nodes
81
70
  RED.httpAdmin.get('/knxUltimatecsv', RED.auth.needsPermission('knxUltimate-config.read'), (req, res) => {
82
71
  try {
@@ -147,23 +136,6 @@ module.exports = (RED) => {
147
136
  }
148
137
  })
149
138
 
150
- // 12/08/2021 Endpoint for deleting the GA persistent file for the current gateway
151
- RED.httpAdmin.get('/deletePersistGAFile', RED.auth.needsPermission('knxUltimate-config.read'), (req, res) => {
152
- try {
153
- if (typeof req.query.serverId !== 'undefined' && req.query.serverId !== null && req.query.serverId !== '') {
154
- try {
155
- const serverId = RED.nodes.getNode(req.query.serverId) // Retrieve node.id of the config node.
156
- const sFile = path.join(serverId.userDir, 'knxpersistvalues', `knxpersist${req.query.serverId}.json`)
157
- fs.unlinkSync(sFile)
158
- } catch (error) { res.json({ error: error.stack }) }
159
- res.json({ error: 'No error' })
160
- } else {
161
- res.json({ error: 'No serverId specified' })
162
- }
163
- } catch (error) {
164
- }
165
- })
166
-
167
139
  // Shared Logger download for legacy nodes and the private Utility profile.
168
140
  const downloadLoggerFile = (req, res) => {
169
141
  try {
@@ -182,19 +154,24 @@ module.exports = (RED) => {
182
154
  res.status(404).json({ error: 'NO_FILE_PATH' })
183
155
  return
184
156
  }
185
- if (!fs.existsSync(filePath)) {
157
+ // The file may still be waiting in the gateway's File Cache Control.
158
+ const storage = storageOf(loggerNode.serverKNX)
159
+ if (!storage.exists(filePath)) {
186
160
  res.status(404).json({ error: 'FILE_NOT_FOUND' })
187
161
  return
188
162
  }
163
+ let content
164
+ try {
165
+ content = storage.readFile(filePath)
166
+ } catch (err) {
167
+ try { RED.log.error(`KNXUltimateLoggerDownload error reading file ${filePath}: ${err.message}`) } catch (e) {}
168
+ res.status(500).json({ error: 'READ_ERROR' })
169
+ return
170
+ }
189
171
  const safeName = path.basename(filePath) || 'knx-logger.xml'
190
172
  res.setHeader('Content-Type', 'application/xml; charset=utf-8')
191
173
  res.setHeader('Content-Disposition', `attachment; filename="${safeName}"`)
192
- const stream = fs.createReadStream(filePath)
193
- stream.on('error', (err) => {
194
- try { RED.log.error(`KNXUltimateLoggerDownload error reading file ${filePath}: ${err.message}`) } catch (e) {}
195
- if (!res.headersSent) res.status(500).json({ error: 'READ_ERROR' })
196
- })
197
- stream.pipe(res)
174
+ res.end(content)
198
175
  } catch (error) {
199
176
  try { RED.log.error(`KNXUltimateLoggerDownload error: ${error.message}`) } catch (e) {}
200
177
  if (!res.headersSent) res.status(500).json({ error: 'UNEXPECTED_ERROR' })
@@ -246,121 +223,6 @@ module.exports = (RED) => {
246
223
  }
247
224
  })
248
225
 
249
- // 2026-06 Reveal all keyring passwords (and the general keyring password) in clear text.
250
- // Used by the "Utility" tab button, enabled only when KNX Secure is selected.
251
- RED.httpAdmin.get('/knxUltimateKeyringDump', RED.auth.needsPermission('knxUltimate-config.read'), async (req, res) => {
252
- try {
253
- let keyringContent = (req.query.keyring || '').toString()
254
- let password = (req.query.pwd || '').toString()
255
- // '__PWRD__' is the Node-RED placeholder for an unchanged password credential: ignore it.
256
- if (password === '__PWRD__') password = ''
257
- // Fill any missing field (keyring and/or password) from the existing config node.
258
- if ((!keyringContent || !password) && req.query.serverId) {
259
- const cfg = RED.nodes.getNode(req.query.serverId)
260
- if (cfg) {
261
- if (!keyringContent) { try { keyringContent = cfg.keyringFileXML || keyringContent } catch (e) { } }
262
- if (!password) { try { password = (cfg.credentials && cfg.credentials.keyringFilePassword) ? cfg.credentials.keyringFilePassword : password } catch (e) { } }
263
- }
264
- }
265
- if (!keyringContent || !password) {
266
- return res.json({ ok: false, error: 'MISSING_KEYRING_OR_PASSWORD' })
267
- }
268
- let Keyring
269
- try {
270
- ({ Keyring } = require('knxultimate/build/secure/keyring'))
271
- } catch (e) {
272
- try { RED.log.error(`KNXUltimate: cannot load Keyring module: ${e.message}`) } catch (err) { }
273
- return res.json({ ok: false, error: 'KEYRING_MODULE_UNAVAILABLE' })
274
- }
275
- const kr = new Keyring()
276
- try {
277
- await kr.load(keyringContent, password)
278
- } catch (e) {
279
- try { RED.log.error(`KNXUltimate: keyring load error: ${e.message}`) } catch (err) { }
280
- return res.json({ ok: false, error: 'KEYRING_LOAD_FAILED' })
281
- }
282
-
283
- const toIAString = (value) => {
284
- if (!value) return ''
285
- return typeof value.toString === 'function' ? value.toString() : String(value)
286
- }
287
- const toBufferString = (value) => {
288
- if (!value) return ''
289
- if (Buffer.isBuffer(value)) return value.toString('hex')
290
- return String(value)
291
- }
292
-
293
- const lines = []
294
- lines.push('================ KNX Secure keyring passwords ================')
295
- lines.push(`Created By: ${kr.getCreatedBy?.() || ''}`)
296
- lines.push(`Created On: ${kr.getCreated?.() || ''}`)
297
- lines.push(`General keyring password: ${password}`)
298
- lines.push('')
299
-
300
- lines.push('Interfaces:')
301
- const interfaceMap = kr.getInterfaces?.()
302
- const interfaces = Array.from(interfaceMap ? interfaceMap.values() : [])
303
- if (interfaces.length === 0) {
304
- lines.push(' (none)')
305
- } else {
306
- interfaces.forEach((iface, idx) => {
307
- lines.push(` [${idx + 1}] ${toIAString(iface.individualAddress) || '(unknown)'} (${iface.type || ''})`)
308
- lines.push(` Host: ${toIAString(iface.host) || ''}`)
309
- lines.push(` User ID: ${typeof iface.userId === 'number' ? iface.userId : ''}`)
310
- lines.push(` Password: ${iface.decryptedPassword || ''}`)
311
- lines.push(` Authentication: ${iface.decryptedAuthentication || ''}`)
312
- })
313
- }
314
- lines.push('')
315
-
316
- lines.push('Backbones:')
317
- const backbones = kr.getBackbones?.() || []
318
- if (backbones.length === 0) {
319
- lines.push(' (none)')
320
- } else {
321
- backbones.forEach((backbone, idx) => {
322
- lines.push(` [${idx + 1}] Multicast: ${backbone.multicastAddress || ''}`)
323
- lines.push(` Key (hex): ${toBufferString(backbone.decryptedKey)}`)
324
- })
325
- }
326
- lines.push('')
327
-
328
- lines.push('Group Addresses:')
329
- const groupAddressMap = kr.getGroupAddresses?.()
330
- const groupAddresses = Array.from(groupAddressMap ? groupAddressMap.values() : [])
331
- if (groupAddresses.length === 0) {
332
- lines.push(' (none)')
333
- } else {
334
- groupAddresses.forEach((group, idx) => {
335
- lines.push(` [${idx + 1}] ${toIAString(group.address) || ''}`)
336
- lines.push(` Key (hex): ${toBufferString(group.decryptedKey)}`)
337
- })
338
- }
339
- lines.push('')
340
-
341
- lines.push('Devices:')
342
- const deviceMap = kr.getDevices?.()
343
- const devices = Array.from(deviceMap ? deviceMap.values() : [])
344
- if (devices.length === 0) {
345
- lines.push(' (none)')
346
- } else {
347
- devices.forEach((device, idx) => {
348
- lines.push(` [${idx + 1}] ${toIAString(device.individualAddress) || ''}`)
349
- lines.push(` Tool Key (hex): ${toBufferString(device.decryptedToolKey)}`)
350
- lines.push(` Management Password: ${device.decryptedManagementPassword || ''}`)
351
- lines.push(` Authentication: ${device.decryptedAuthentication || ''}`)
352
- lines.push(` Serial Number: ${device.serialNumber || ''}`)
353
- })
354
- }
355
- lines.push('================ End of keyring passwords ================')
356
-
357
- res.json({ ok: true, dump: lines.join('\n') })
358
- } catch (error) {
359
- try { RED.log.error(`KNXUltimate: knxUltimateKeyringDump error: ${error.message}`) } catch (e) { }
360
- res.json({ ok: false, error: 'UNEXPECTED_ERROR' })
361
- }
362
- })
363
-
364
226
  RED.httpAdmin.get('/knxUltimateKeyringDataSecureGAs', RED.auth.needsPermission('knxUltimate-config.read'), async (req, res) => {
365
227
  try {
366
228
  let keyringContent = (req.query.keyring || '').toString()
@@ -4,6 +4,16 @@
4
4
  const optionalNumberValidator = function (value) {
5
5
  return (value === '' || value === null || typeof value === 'undefined') ? true : RED.validators.number()(value)
6
6
  }
7
+ // File Cache Control: the custom interval must be between 5 seconds and 30 days.
8
+ const fileCacheIntervalValidator = function (value) {
9
+ const field = (name, fallback) => {
10
+ const element = $("#node-config-input-" + name);
11
+ return element.length ? element.val() : fallback;
12
+ };
13
+ if (field('fileCacheMode', this.fileCacheMode) !== 'custom') return true;
14
+ const seconds = Number(value) * ({ s: 1, m: 60, h: 3600, d: 86400 }[field('fileCacheIntervalUnit', this.fileCacheIntervalUnit)] || 1);
15
+ return Number.isFinite(seconds) && seconds >= 5 && seconds <= 30 * 86400;
16
+ }
7
17
 
8
18
  RED.nodes.registerType('knxUltimate-config', {
9
19
  category: 'config',
@@ -15,11 +25,15 @@
15
25
  hostProtocol: { value: "Auto", required: false }, // TunnelUDP/TunnelTCP/Multicast
16
26
  // enable this option to suppress the acknowledge flag with outgoing L_Data.req requests. LoxOne needs this
17
27
  suppressACKRequest: { value: false },
28
+ waitForTunnelingAck: { value: "auto" }, // TunnelUDP: auto follows suppressACKRequest, as the KNX engine does
18
29
  csv: { value: "", required: false },
19
30
  KNXEthInterface: { value: "Auto" },
20
31
  KNXEthInterfaceManuallyInput: { value: "" },
21
32
  stopETSImportIfNoDatapoint: { value: "fake" },
22
33
  loglevel: { value: "error" },
34
+ fileCacheMode: { value: "freshness" },
35
+ fileCacheInterval: { value: 10, validate: fileCacheIntervalValidator },
36
+ fileCacheIntervalUnit: { value: "m" },
23
37
  name: { value: "KNX Gateway" },
24
38
  delaybetweentelegrams: { value: 25, required: false, validate: RED.validators.number() },
25
39
  ignoreTelegramsWithRepeatedFlag: { value: false, required: false },
@@ -164,13 +178,13 @@
164
178
  active: (node.knxSecureSelected === true || node.knxSecureSelected === 'true') ? 1 : 0,
165
179
  activate: function (event, ui) {
166
180
  node.knxSecureSelected = $(ui.newTab).index() === 1;
167
- try { updateTunnelIAVisibility(); updatePhysAddrVisibility(); updateSecureMulticastHint(); enforceProtocolFromIP(); updateRevealKeyringButton(); } catch (e) { }
181
+ try { updateTunnelIAVisibility(); updatePhysAddrVisibility(); updateSecureMulticastHint(); enforceProtocolFromIP(); } catch (e) { }
168
182
  }
169
183
  });
170
184
  // Ensure the correct tab is enforced after all UI init
171
185
  try {
172
186
  const initialActive = (node.knxSecureSelected === true || node.knxSecureSelected === 'true') ? 1 : 0;
173
- setTimeout(() => { $("#tabsMain").tabs("option", "active", initialActive); updateTunnelIAVisibility(); updatePhysAddrVisibility(); updateSecureMulticastHint(); enforceProtocolFromIP(); updateRevealKeyringButton(); }, 0);
187
+ setTimeout(() => { $("#tabsMain").tabs("option", "active", initialActive); updateTunnelIAVisibility(); updatePhysAddrVisibility(); updateSecureMulticastHint(); enforceProtocolFromIP(); }, 0);
174
188
  } catch (e) { }
175
189
 
176
190
  // Keyring ACE editor setup
@@ -973,123 +987,32 @@
973
987
  try { updateSecureModeAvailability(); } catch (e) { }
974
988
 
975
989
 
976
- // NOTE: Do not auto-delete persisted GA cache on editor open.
977
- // Users can explicitly clear it from the Utility tab.
978
-
979
990
  // 06/07/2023 Tabs
980
991
  // *****************************
981
992
  $("#tabs").tabs();
982
993
  // *****************************
983
994
 
984
- var sRetDebugText = "";
985
- $("#getinfocam").click(function () {
986
- sRetDebugText = "";
987
- $("#divDebugText").show();
988
- for (const [key, value] of Object.entries(node)) {
989
- sRetDebugText += (`-> ${key}: ${value}\r`);
990
- }
991
- $("#debugText").val(sRetDebugText); // Store the config-node);
992
- });
993
- $("#getallgaused").click(function () {
994
- sRetDebugText = "";
995
- $("#divDebugText").show();
996
- let aFound = [];
997
- RED.nodes.eachNode(function (node) {
998
- if (!aFound.includes(node.topic)) {
999
- aFound.push(node.topic);
1000
- sRetDebugText += node.topic + "\r"
1001
- }
1002
- });
1003
- sRetDebugText = node._('knxUltimate-config.utility.copy_ga_router') + "\r" + sRetDebugText;
1004
- $("#debugText").val(sRetDebugText); // Store the config-node);
1005
- });
1006
-
1007
- $("#clearpersistga").click(function () {
1008
- try {
1009
- const confirmText = node._("knxUltimate-config.utility.clear_persist_confirm");
1010
- const okText = node._("knxUltimate-config.utility.clear_persist_ok");
1011
- const cancelText = node._("knxUltimate-config.utility.clear_persist_cancel");
1012
- const doneText = node._("knxUltimate-config.utility.clear_persist_done");
1013
- const failText = node._("knxUltimate-config.utility.clear_persist_fail");
1014
-
1015
- var dlg = RED.notify(confirmText, {
1016
- modal: true,
1017
- fixed: true,
1018
- type: "warning",
1019
- buttons: [
1020
- {
1021
- text: cancelText,
1022
- click: function () { dlg.close(); }
1023
- },
1024
- {
1025
- text: okText,
1026
- class: "primary",
1027
- click: function () {
1028
- dlg.close();
1029
- $.getJSON("deletePersistGAFile?serverId=" + node.id, (data) => {
1030
- RED.notify(doneText, { type: "success", timeout: 2500 });
1031
- }).fail(function (jqXHR, textStatus, errorThrown) {
1032
- const err = (errorThrown || textStatus || "").toString();
1033
- RED.notify(failText + (err ? (": " + err) : ""), { type: "error", timeout: 5000 });
1034
- });
1035
- }
1036
- }
1037
- ]
1038
- });
1039
- } catch (e) { }
1040
- });
1041
-
1042
- // Utility: reveal all keyring passwords in clear text.
1043
- // Enabled only when KNX Secure is selected.
1044
- function updateRevealKeyringButton() {
1045
- try {
1046
- const isSecure = isSecureTabActive();
1047
- $("#revealkeyringpwd").prop('disabled', !isSecure)
1048
- .css('opacity', isSecure ? 1 : 0.5)
1049
- .css('cursor', isSecure ? 'pointer' : 'not-allowed');
1050
- } catch (e) { }
1051
- }
1052
- updateRevealKeyringButton();
1053
-
1054
- $("#revealkeyringpwd").click(function () {
1055
- if (!isSecureTabActive()) return;
1056
- try {
1057
- const keyring = (node._keyringEditor && typeof node._keyringEditor.getValue === 'function')
1058
- ? node._keyringEditor.getValue()
1059
- : $("#node-config-input-keyringFileXML").val();
1060
- const pwd = $("#node-config-input-keyringFilePassword").val();
1061
- const params = { serverId: node.id };
1062
- if (typeof keyring === 'string' && keyring.trim() !== '') params.keyring = keyring.trim();
1063
- // '__PWRD__' is the Node-RED placeholder for an unchanged password credential:
1064
- // never send it, so the backend falls back to the real stored credential.
1065
- if (typeof pwd === 'string' && pwd.trim() !== '' && pwd !== '__PWRD__') params.pwd = pwd.trim();
1066
- $("#divDebugText").show();
1067
- $("#debugText").val(node._('knxUltimate-config.utility.reveal_keyring_loading'));
1068
- const scrollToDebugText = function () {
1069
- try {
1070
- const el = document.getElementById('debugText');
1071
- if (el && typeof el.scrollIntoView === 'function') {
1072
- el.scrollIntoView({ behavior: 'smooth', block: 'center' });
1073
- }
1074
- } catch (e) { }
1075
- };
1076
- scrollToDebugText();
1077
- $.getJSON('knxUltimateKeyringDump', params, (data) => {
1078
- if (data && data.ok) {
1079
- $("#debugText").val(data.dump);
1080
- } else {
1081
- const code = (data && data.error) ? data.error : 'UNEXPECTED_ERROR';
1082
- $("#debugText").val(node._('knxUltimate-config.utility.reveal_keyring_fail') + ' (' + code + ')');
1083
- }
1084
- scrollToDebugText();
1085
- }).fail(function (jqXHR, textStatus, errorThrown) {
1086
- const err = (errorThrown || textStatus || '').toString();
1087
- $("#debugText").val(node._('knxUltimate-config.utility.reveal_keyring_fail') + (err ? (': ' + err) : ''));
1088
- });
1089
- } catch (e) { }
1090
- });
1091
-
1092
-
995
+ // Gateways saved by older versions lack these options: Node-RED then leaves
996
+ // their fields empty, so show the defaults the runtime uses.
997
+ const applyMissingDefault = (field, value) => {
998
+ const $field = $("#node-config-input-" + field);
999
+ if ($field.val() === null || $field.val() === '') $field.val(value);
1000
+ };
1001
+ applyMissingDefault('fileCacheMode', 'freshness');
1002
+ applyMissingDefault('fileCacheInterval', 10);
1003
+ applyMissingDefault('fileCacheIntervalUnit', 'm');
1004
+ applyMissingDefault('waitForTunnelingAck', 'auto');
1005
+
1006
+ // File Cache Control: show the custom interval and the "writing disabled" tip.
1007
+ const syncFileCacheRows = () => {
1008
+ const mode = $("#node-config-input-fileCacheMode").val();
1009
+ $("#knxUltimate-fileCacheInterval-row").toggle(mode === 'custom');
1010
+ $("#knxUltimate-fileCacheDisabled-tip").toggle(mode === 'disabled');
1011
+ $("#node-config-input-fileCacheInterval").trigger('change');
1012
+ };
1013
+ $("#knxUltimate-fileCacheDisabled-tip").text(node._("knxUltimate-config.file_cache.disabled_tip"));
1014
+ $("#node-config-input-fileCacheMode, #node-config-input-fileCacheIntervalUnit").on('change', syncFileCacheRows);
1015
+ syncFileCacheRows();
1093
1016
 
1094
1017
  },
1095
1018
  oneditcancel: function () {
@@ -1362,7 +1285,7 @@
1362
1285
  <li><a href="#tabs-1"><i class="fa fa-list-ol"></i> <span data-i18n="knxUltimate-config.tabs.configuration"></span></a></li>
1363
1286
  <li><a href="#tabs-2"><i class="fa fa-braille"></i> <span data-i18n="knxUltimate-config.tabs.advanced"></span></a></li>
1364
1287
  <li><a href="#tabs-3"><i class="fa fa-code"></i> <span data-i18n="knxUltimate-config.tabs.ets_import"></span></a></li>
1365
- <li><a href="#tabs-4"><i class="fa fa-key"></i> <span data-i18n="knxUltimate-config.tabs.utility"></span></a></li>
1288
+ <li><a href="#tabs-4"><i class="fa fa-hdd-o"></i> <span data-i18n="knxUltimate-config.tabs.file_cache"></span></a></li>
1366
1289
  </ul>
1367
1290
  <div id="tabs-1">
1368
1291
  <p>
@@ -1480,6 +1403,20 @@
1480
1403
  <span data-i18n="knxUltimate-config.advanced.suppress_ack"></span>
1481
1404
  </label>
1482
1405
  </div>
1406
+ <div class="form-tips" data-i18n="knxUltimate-config.advanced.suppress_ack_help"></div>
1407
+
1408
+ <div class="form-row">
1409
+ <label for="node-config-input-waitForTunnelingAck" style="width:auto;">
1410
+ <i class="fa fa-exchange"></i>
1411
+ <span data-i18n="knxUltimate-config.advanced.wait_tunneling_ack"></span>
1412
+ </label>
1413
+ <select id="node-config-input-waitForTunnelingAck" style="width:60%;">
1414
+ <option value="auto" data-i18n="knxUltimate-config.advanced.wait_tunneling_ack_auto"></option>
1415
+ <option value="yes" data-i18n="knxUltimate-config.advanced.wait_tunneling_ack_yes"></option>
1416
+ <option value="no" data-i18n="knxUltimate-config.advanced.wait_tunneling_ack_no"></option>
1417
+ </select>
1418
+ </div>
1419
+ <div class="form-tips" data-i18n="knxUltimate-config.advanced.wait_tunneling_ack_help"></div>
1483
1420
 
1484
1421
  <div class="form-row form-row-checkbox">
1485
1422
  <input type="checkbox" id="node-config-input-enableFlowBubbles"
@@ -1500,22 +1437,6 @@
1500
1437
  <input type="number" id="node-config-input-delaybetweentelegrams" style="width:20%">
1501
1438
  </div>
1502
1439
 
1503
- <div class="form-row">
1504
- <label for="node-config-input-loglevel">
1505
- <i class="fa fa-question-circle"></i>
1506
- <span data-i18n="knxUltimate-config.advanced.log_level"></span>
1507
- </label>
1508
- <select id="node-config-input-loglevel" style="width:40%;">
1509
- <option value="disable" data-i18n="knxUltimate-config.advanced.select_silent"></option>
1510
- <option value="error" data-i18n="knxUltimate-config.advanced.select_error"></option>
1511
- <option value="warn" data-i18n="knxUltimate-config.advanced.select_warning"></option>
1512
- <option value="info" data-i18n="knxUltimate-config.advanced.select_info"></option>
1513
- <option value="debug" data-i18n="knxUltimate-config.advanced.select_debug"></option>
1514
- </select>
1515
- <!-- <// DEBUG LEVELS: success < debug < info < warn < error < disable
1516
- </div> -->
1517
- </div>
1518
-
1519
1440
  <div class="form-row">
1520
1441
  <label> <span style="font-weight:bold">Node Status</span>
1521
1442
  </label>
@@ -1602,36 +1523,53 @@
1602
1523
  </p>
1603
1524
  </div>
1604
1525
 
1605
- <div id="tabs-4">
1606
- <p>
1607
- <div class="form-row">
1608
- <label><i class="fa fa-sign-in"></i> <span data-i18n="knxUltimate-config.utility.gather_debug"></span></label>
1609
- <input type="button" id="getinfocam" class="ui-button ui-corner-all ui-widget"
1610
- style="background-color:#AEE1FF;width:150px" data-i18n="[value]knxUltimate-config.utility.read_button">
1611
- </div>
1612
- <div class="form-row">
1613
- <label><i class="fa fa-sign-in"></i> <span data-i18n="knxUltimate-config.utility.get_all_used_ga"></span></label>
1614
- <input type="button" id="getallgaused" class="ui-button ui-corner-all ui-widget"
1615
- style="background-color:#AEE1FF;width:150px" data-i18n="[value]knxUltimate-config.utility.read_button">
1616
- </div>
1526
+ <div id="tabs-4">
1527
+ <p>
1528
+ <div class="form-row">
1529
+ <label for="node-config-input-loglevel">
1530
+ <i class="fa fa-question-circle"></i>
1531
+ <span data-i18n="knxUltimate-config.advanced.log_level"></span>
1532
+ </label>
1533
+ <select id="node-config-input-loglevel" style="width:40%;">
1534
+ <option value="disable" data-i18n="knxUltimate-config.advanced.select_silent"></option>
1535
+ <option value="error" data-i18n="knxUltimate-config.advanced.select_error"></option>
1536
+ <option value="warn" data-i18n="knxUltimate-config.advanced.select_warning"></option>
1537
+ <option value="info" data-i18n="knxUltimate-config.advanced.select_info"></option>
1538
+ <option value="debug" data-i18n="knxUltimate-config.advanced.select_debug"></option>
1539
+ </select>
1540
+ </div>
1617
1541
 
1618
- <div class="form-row">
1619
- <label><i class="fa fa-trash"></i> <span data-i18n="knxUltimate-config.utility.clear_persist_label"></span></label>
1620
- <input type="button" id="clearpersistga" class="ui-button ui-corner-all ui-widget"
1621
- style="background-color:#FFD2D2;width:150px" data-i18n="[value]knxUltimate-config.utility.clear_persist_button">
1622
- </div>
1542
+ <div class="form-row">
1543
+ <label for="node-config-input-fileCacheMode">
1544
+ <i class="fa fa-hdd-o"></i>
1545
+ <span data-i18n="knxUltimate-config.file_cache.mode"></span>
1546
+ </label>
1547
+ <select id="node-config-input-fileCacheMode" style="width:60%;">
1548
+ <option value="freshness" data-i18n="knxUltimate-config.file_cache.mode_freshness"></option>
1549
+ <option value="balanced" data-i18n="knxUltimate-config.file_cache.mode_balanced"></option>
1550
+ <option value="disabled" data-i18n="knxUltimate-config.file_cache.mode_disabled"></option>
1551
+ <option value="custom" data-i18n="knxUltimate-config.file_cache.mode_custom"></option>
1552
+ </select>
1553
+ </div>
1554
+ <div class="form-tips knx-file-cache-disabled-tip" id="knxUltimate-fileCacheDisabled-tip" style="display:none;"></div>
1623
1555
 
1624
- <div class="form-row">
1625
- <label><i class="fa fa-key"></i> <span data-i18n="knxUltimate-config.utility.reveal_keyring_label"></span></label>
1626
- <input type="button" id="revealkeyringpwd" class="ui-button ui-corner-all ui-widget"
1627
- style="background-color:#FFE9B3;width:150px" data-i18n="[value]knxUltimate-config.utility.reveal_keyring_button">
1628
- </div>
1556
+ <div class="form-row" id="knxUltimate-fileCacheInterval-row" style="display:none;">
1557
+ <label for="node-config-input-fileCacheInterval">
1558
+ <i class="fa fa-clock-o"></i>
1559
+ <span data-i18n="knxUltimate-config.file_cache.interval"></span>
1560
+ </label>
1561
+ <input type="number" id="node-config-input-fileCacheInterval" min="1" style="width:100px;">
1562
+ <select id="node-config-input-fileCacheIntervalUnit" style="width:120px;">
1563
+ <option value="s" data-i18n="knxUltimate-config.file_cache.unit_seconds"></option>
1564
+ <option value="m" data-i18n="knxUltimate-config.file_cache.unit_minutes"></option>
1565
+ <option value="h" data-i18n="knxUltimate-config.file_cache.unit_hours"></option>
1566
+ <option value="d" data-i18n="knxUltimate-config.file_cache.unit_days"></option>
1567
+ </select>
1568
+ </div>
1629
1569
 
1630
- <div class="form-row" id="divDebugText" style="display: none;">
1631
- <textarea rows="10" id="debugText" style="width:100%"></textarea>
1632
- </div>
1633
- </p>
1634
- </div>
1570
+ <div class="form-tips" data-i18n="[html]knxUltimate-config.file_cache.description"></div>
1571
+ </p>
1572
+ </div>
1635
1573
  </div>
1636
1574
 
1637
1575
  </div>