node-red-contrib-knx-ultimate 8.1.0 → 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.
- package/CHANGELOG.md +17 -0
- package/nodes/commonFunctions.js +13 -151
- package/nodes/knxUltimate-config.html +96 -158
- package/nodes/knxUltimate-config.js +64 -58
- package/nodes/knxUltimate.html +4 -0
- package/nodes/knxUltimateUtility.html +7 -0
- package/nodes/knxUltimateViewer.html +3 -0
- package/nodes/knxUltimateViewer.js +3 -1
- package/nodes/locales/de/knxUltimate-config.html +8 -5
- package/nodes/locales/de/knxUltimate-config.json +22 -20
- package/nodes/locales/de/knxUltimate.html +2 -0
- package/nodes/locales/de/knxUltimateUtility.html +6 -0
- package/nodes/locales/de/knxUltimateViewer.html +2 -0
- package/nodes/locales/en/knxUltimate-config.html +8 -5
- package/nodes/locales/en/knxUltimate-config.json +22 -20
- package/nodes/locales/en/knxUltimate.html +2 -0
- package/nodes/locales/en/knxUltimateUtility.html +6 -0
- package/nodes/locales/en/knxUltimateViewer.html +2 -0
- package/nodes/locales/es/knxUltimate-config.html +12 -6
- package/nodes/locales/es/knxUltimate-config.json +22 -20
- package/nodes/locales/es/knxUltimate.html +2 -0
- package/nodes/locales/es/knxUltimateUtility.html +6 -0
- package/nodes/locales/es/knxUltimateViewer.html +2 -0
- package/nodes/locales/fr/knxUltimate-config.html +10 -7
- package/nodes/locales/fr/knxUltimate-config.json +22 -20
- package/nodes/locales/fr/knxUltimate.html +2 -0
- package/nodes/locales/fr/knxUltimateUtility.html +6 -0
- package/nodes/locales/fr/knxUltimateViewer.html +2 -0
- package/nodes/locales/it/knxUltimate-config.html +8 -5
- package/nodes/locales/it/knxUltimate-config.json +22 -20
- package/nodes/locales/it/knxUltimate.html +2 -0
- package/nodes/locales/it/knxUltimateUtility.html +6 -0
- package/nodes/locales/it/knxUltimateViewer.html +2 -0
- package/nodes/locales/zh-CN/knxUltimate-config.html +8 -5
- package/nodes/locales/zh-CN/knxUltimate-config.json +22 -20
- package/nodes/locales/zh-CN/knxUltimate.html +2 -0
- package/nodes/locales/zh-CN/knxUltimateUtility.html +6 -0
- package/nodes/locales/zh-CN/knxUltimateViewer.html +2 -0
- package/nodes/utils/fileStorage.js +376 -0
- package/nodes/utils/knxUtilityProfiles/runtime/autoresponder.js +15 -19
- package/nodes/utils/knxUtilityProfiles/runtime/logger.js +7 -4
- package/nodes/utils/knxUtilityProfiles/runtime/scenecontroller.js +12 -44
- package/nodes/utils/knxViewerHistory.js +49 -77
- package/package.json +2 -2
- package/resources/htmlUtils.js +24 -2
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
// Private KNX Utility profile. Independent of the legacy palette entry point.
|
|
2
2
|
const createProfileTimers = require('./profileTimers')
|
|
3
3
|
const loggerClass = require('../../sysLogger')
|
|
4
|
+
const { storageOf } = require('../../fileStorage')
|
|
4
5
|
|
|
5
6
|
module.exports = function (RED) {
|
|
6
7
|
function knxUltimateSceneController (config) {
|
|
7
|
-
const fs = require('fs')
|
|
8
8
|
const path = require('path')
|
|
9
9
|
|
|
10
10
|
RED.nodes.createNode(this, config)
|
|
@@ -72,43 +72,9 @@ module.exports = function (RED) {
|
|
|
72
72
|
})
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
} catch (e) {
|
|
79
|
-
// Path does not exist
|
|
80
|
-
if (e.code === 'ENOENT') {
|
|
81
|
-
// Try and create it
|
|
82
|
-
try {
|
|
83
|
-
try {
|
|
84
|
-
fs.mkdirSync(aPath, { recursive: true })
|
|
85
|
-
if (node.sysLogger !== undefined && node.sysLogger !== null) node.sysLogger.info('knxUltimate-Scene Controller: created directory path: ' + aPath)
|
|
86
|
-
} catch (error) {
|
|
87
|
-
if (node.sysLogger !== undefined && node.sysLogger !== null) node.sysLogger.error('knxUltimate-Scene Controller: failed to access path:: ' + aPath + ' : ' + error)
|
|
88
|
-
return false
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
return true
|
|
92
|
-
} catch (e) {
|
|
93
|
-
if (node.sysLogger !== undefined && node.sysLogger !== null) node.sysLogger.error('knxUltimate-Scene Controller: failed to create path: ' + aPath + ' : ' + e)
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
// Otherwise failure
|
|
97
|
-
return false
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// This stores all scenes values, that are been saved.
|
|
102
|
-
try {
|
|
103
|
-
setupDirectory(node.userDir)
|
|
104
|
-
} catch (error) { }
|
|
105
|
-
if (!setupDirectory(node.userDir + '/scenecontroller')) {
|
|
106
|
-
if (node.sysLogger !== undefined && node.sysLogger !== null) node.sysLogger.error('knxUltimate-Scene Controller: Unable to set up permanent files directory: ' + node.userDir + '/scenecontroller')
|
|
107
|
-
updateStatus({ fill: 'red', shape: 'dot', text: 'Unable to setup permanent files directory' })
|
|
108
|
-
} else {
|
|
109
|
-
|
|
110
|
-
}
|
|
111
|
-
|
|
75
|
+
// This stores all scenes values, that are been saved. The gateway's
|
|
76
|
+
// File Cache Control decides when they reach the disk.
|
|
77
|
+
const storage = storageOf(node.serverKNX)
|
|
112
78
|
const scenePath = path.join(node.userDir, 'scenecontroller', 'SceneController_' + node.id)
|
|
113
79
|
const definitionPath = scenePath + '.config.json'
|
|
114
80
|
const sceneSchema = rules => rules.map(rule => ({ topic: rule.topic, dpt: rule.dpt || '' }))
|
|
@@ -123,20 +89,22 @@ module.exports = function (RED) {
|
|
|
123
89
|
// original configured values using the separate, ID-based definition file.
|
|
124
90
|
try {
|
|
125
91
|
let oldDefinition
|
|
126
|
-
try { oldDefinition = JSON.parse(
|
|
92
|
+
try { oldDefinition = JSON.parse(storage.readFile(definitionPath, 'utf8')) } catch (error) {
|
|
127
93
|
if (error.code !== 'ENOENT') throw error
|
|
128
94
|
}
|
|
129
95
|
let oldScene
|
|
130
|
-
try { oldScene = JSON.parse(
|
|
96
|
+
try { oldScene = JSON.parse(storage.readFile(scenePath, 'utf8')) } catch (error) {
|
|
131
97
|
if (error.code !== 'ENOENT') throw error
|
|
132
98
|
}
|
|
133
99
|
const definitionChanged = oldDefinition && JSON.stringify(oldDefinition.rules) !== JSON.stringify(configuredDefinition)
|
|
134
100
|
const schemaChanged = oldScene && (!Array.isArray(oldScene) || JSON.stringify(sceneSchema(oldScene)) !== JSON.stringify(sceneSchema(configuredDefinition)))
|
|
135
101
|
if (definitionChanged || schemaChanged) {
|
|
136
102
|
usePersistedScene = false
|
|
137
|
-
if (oldScene)
|
|
103
|
+
if (oldScene) storage.deleteFile(scenePath)
|
|
138
104
|
}
|
|
139
|
-
|
|
105
|
+
// Rewrite the definition only when it changes.
|
|
106
|
+
const definition = JSON.stringify({ version: 1, rules: configuredDefinition })
|
|
107
|
+
if (!oldDefinition || JSON.stringify(oldDefinition) !== definition) storage.writeFile(definitionPath, definition)
|
|
140
108
|
} catch (error) {
|
|
141
109
|
usePersistedScene = false
|
|
142
110
|
updateStatus({ fill: 'red', shape: 'dot', text: 'Unable to prepare scene storage: ' + error.message })
|
|
@@ -244,7 +212,7 @@ module.exports = function (RED) {
|
|
|
244
212
|
let oSavedRules = null
|
|
245
213
|
try {
|
|
246
214
|
if (usePersistedScene) {
|
|
247
|
-
oSavedRules = JSON.parse(
|
|
215
|
+
oSavedRules = JSON.parse(storage.readFile(scenePath, 'utf8'))
|
|
248
216
|
}
|
|
249
217
|
} catch (error) { }
|
|
250
218
|
|
|
@@ -397,7 +365,7 @@ module.exports = function (RED) {
|
|
|
397
365
|
}
|
|
398
366
|
node.setNodeStatus({ fill: 'blue', shape: 'dot', text: 'Saved scene', payload: '', GA: '', dpt: '', devicename: '' })
|
|
399
367
|
try {
|
|
400
|
-
|
|
368
|
+
storage.writeFile(scenePath, JSON.stringify(node.rules, null, 2))
|
|
401
369
|
usePersistedScene = true
|
|
402
370
|
} catch (error) {
|
|
403
371
|
node.setNodeStatus({ fill: 'red', shape: 'dot', text: 'Error saving scene. Unable to access filesystem.', payload: '', GA: '', dpt: '', devicename: node.name })
|
|
@@ -1,9 +1,6 @@
|
|
|
1
|
-
const fs = require('fs')
|
|
2
1
|
const path = require('path')
|
|
3
2
|
const crypto = require('crypto')
|
|
4
|
-
const
|
|
5
|
-
const { once } = require('events')
|
|
6
|
-
const { finished } = require('stream/promises')
|
|
3
|
+
const { directStorage } = require('./fileStorage')
|
|
7
4
|
|
|
8
5
|
const HISTORY_TTL_MS = 24 * 60 * 60 * 1000
|
|
9
6
|
const HOUR_MS = 60 * 60 * 1000
|
|
@@ -11,6 +8,7 @@ const MAINTENANCE_MS = 60 * 1000
|
|
|
11
8
|
const CHUNK_BYTES = 64 * 1024
|
|
12
9
|
const MAX_LINE_BYTES = 1024 * 1024
|
|
13
10
|
const FILE_PATTERN = /^(\d+)\.jsonl$/
|
|
11
|
+
const FILE_OPTIONS = { mode: 0o600, dirMode: 0o700, newlineDelimited: true }
|
|
14
12
|
|
|
15
13
|
// Canonical JSON makes structured values independent of object key order and
|
|
16
14
|
// treats a Buffer identically before and after its JSON round trip.
|
|
@@ -56,21 +54,30 @@ function parseEntry (line) {
|
|
|
56
54
|
return null
|
|
57
55
|
}
|
|
58
56
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
let
|
|
64
|
-
stream.on('error', error => { streamError = error; lines.close() })
|
|
57
|
+
// Files are read through the storage, so data not yet written to disk is included.
|
|
58
|
+
async function * forwardLines (storage, filename) {
|
|
59
|
+
const snapshot = await storage.openSnapshot(filename)
|
|
60
|
+
let position = 0
|
|
61
|
+
let pending = Buffer.alloc(0)
|
|
65
62
|
try {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
63
|
+
while (position < snapshot.size) {
|
|
64
|
+
const chunk = Buffer.allocUnsafe(Math.min(CHUNK_BYTES, snapshot.size - position))
|
|
65
|
+
const { bytesRead } = await snapshot.read(chunk, 0, chunk.length, position)
|
|
66
|
+
if (!bytesRead) break
|
|
67
|
+
position += bytesRead
|
|
68
|
+
const data = Buffer.concat([pending, chunk.subarray(0, bytesRead)])
|
|
69
|
+
let start = 0
|
|
70
|
+
for (let index = data.indexOf(10); index !== -1; index = data.indexOf(10, start)) {
|
|
71
|
+
if (index - start > MAX_LINE_BYTES) throw new Error('KNX Viewer history contains an oversized entry')
|
|
72
|
+
if (index > start) yield data.subarray(start, index).toString('utf8')
|
|
73
|
+
start = index + 1
|
|
74
|
+
}
|
|
75
|
+
pending = data.subarray(start)
|
|
76
|
+
if (pending.length > MAX_LINE_BYTES) throw new Error('KNX Viewer history contains an oversized entry')
|
|
69
77
|
}
|
|
70
|
-
if (
|
|
78
|
+
if (pending.length) yield pending.toString('utf8')
|
|
71
79
|
} finally {
|
|
72
|
-
|
|
73
|
-
stream.destroy()
|
|
80
|
+
await snapshot.close()
|
|
74
81
|
}
|
|
75
82
|
}
|
|
76
83
|
|
|
@@ -95,30 +102,26 @@ async function * reverseLines (handle, size) {
|
|
|
95
102
|
if (pending.length) yield pending.toString('utf8')
|
|
96
103
|
}
|
|
97
104
|
|
|
98
|
-
async function repairIncompleteTail (filename) {
|
|
99
|
-
const
|
|
105
|
+
async function repairIncompleteTail (storage, filename) {
|
|
106
|
+
const snapshot = await storage.openSnapshot(filename)
|
|
107
|
+
let complete = true
|
|
100
108
|
try {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
position -= length
|
|
106
|
-
const chunk = Buffer.allocUnsafe(length)
|
|
107
|
-
const { bytesRead } = await handle.read(chunk, 0, length, position)
|
|
108
|
-
const newline = chunk.subarray(0, bytesRead).lastIndexOf(10)
|
|
109
|
-
if (newline !== -1) {
|
|
110
|
-
await handle.truncate(position + newline + 1)
|
|
111
|
-
return
|
|
112
|
-
}
|
|
109
|
+
if (snapshot.size) {
|
|
110
|
+
const last = Buffer.alloc(1)
|
|
111
|
+
await snapshot.read(last, 0, 1, snapshot.size - 1)
|
|
112
|
+
complete = last[0] === 10
|
|
113
113
|
}
|
|
114
|
-
await handle.truncate(0)
|
|
115
114
|
} finally {
|
|
116
|
-
await
|
|
115
|
+
await snapshot.close()
|
|
117
116
|
}
|
|
117
|
+
if (complete) return
|
|
118
|
+
const content = storage.readFile(filename)
|
|
119
|
+
storage.writeFile(filename, content.subarray(0, content.lastIndexOf(10) + 1), FILE_OPTIONS)
|
|
118
120
|
}
|
|
119
121
|
|
|
120
122
|
/** Persistent, paginated state changes for one Viewer and one KNX gateway. */
|
|
121
|
-
|
|
123
|
+
// Every file operation goes through the gateway's File Cache Control storage.
|
|
124
|
+
function createViewerHistory ({ userDir, nodeId, gatewayId, onError, now = Date.now, storage = directStorage }) {
|
|
122
125
|
// Hashing both identifiers prevents path traversal and filesystem name limits.
|
|
123
126
|
const identity = crypto.createHash('sha256').update(JSON.stringify([String(nodeId), String(gatewayId || '')])).digest('hex')
|
|
124
127
|
const directory = path.join(userDir, 'knxultimatestorage', 'viewer', identity)
|
|
@@ -158,7 +161,7 @@ function createViewerHistory ({ userDir, nodeId, gatewayId, onError, now = Date.
|
|
|
158
161
|
}
|
|
159
162
|
|
|
160
163
|
async function files () {
|
|
161
|
-
return
|
|
164
|
+
return storage.listDirectory(directory)
|
|
162
165
|
.filter(name => FILE_PATTERN.test(name))
|
|
163
166
|
.map(name => ({ name, hour: Number(name.slice(0, -6)), filename: path.join(directory, name) }))
|
|
164
167
|
.filter(file => Number.isSafeInteger(file.hour))
|
|
@@ -174,46 +177,17 @@ function createViewerHistory ({ userDir, nodeId, gatewayId, onError, now = Date.
|
|
|
174
177
|
})
|
|
175
178
|
}
|
|
176
179
|
|
|
177
|
-
async function compact (file, cutoff) {
|
|
178
|
-
const temporary = `${file.filename}.${crypto.randomUUID()}.tmp`
|
|
179
|
-
const output = fs.createWriteStream(temporary, { flags: 'wx', mode: 0o600 })
|
|
180
|
-
// Observe errors immediately, including ones raised while reading input.
|
|
181
|
-
const completion = finished(output)
|
|
182
|
-
completion.catch(() => {})
|
|
183
|
-
let removed = false
|
|
184
|
-
let count = 0
|
|
185
|
-
try {
|
|
186
|
-
for await (const line of forwardLines(file.filename)) {
|
|
187
|
-
const entry = parseEntry(line)
|
|
188
|
-
if (!entry || entry.timestampMs < cutoff) { removed = true; continue }
|
|
189
|
-
count++
|
|
190
|
-
if (!output.write(`${line}\n`)) await once(output, 'drain')
|
|
191
|
-
}
|
|
192
|
-
output.end()
|
|
193
|
-
await completion
|
|
194
|
-
if (!removed) await fs.promises.unlink(temporary)
|
|
195
|
-
else if (!count) {
|
|
196
|
-
await fs.promises.unlink(file.filename)
|
|
197
|
-
await fs.promises.unlink(temporary)
|
|
198
|
-
hours.delete(file.hour)
|
|
199
|
-
} else await fs.promises.rename(temporary, file.filename)
|
|
200
|
-
} catch (error) {
|
|
201
|
-
output.destroy()
|
|
202
|
-
await completion.catch(() => {})
|
|
203
|
-
await fs.promises.unlink(temporary).catch(() => {})
|
|
204
|
-
throw error
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
180
|
async function prune (force = false) {
|
|
209
181
|
const timestamp = clock()
|
|
210
182
|
if (!force && timestamp - lastPrune < MAINTENANCE_MS) return
|
|
211
183
|
const cutoff = timestamp - HISTORY_TTL_MS
|
|
184
|
+
// Only whole expired hours are deleted: rewriting a partially expired hour
|
|
185
|
+
// would wear the disk, and queries already skip its expired entries.
|
|
212
186
|
for (const file of await files()) {
|
|
213
187
|
if ((file.hour + 1) * HOUR_MS <= cutoff) {
|
|
214
|
-
|
|
188
|
+
storage.deleteFile(file.filename)
|
|
215
189
|
hours.delete(file.hour)
|
|
216
|
-
}
|
|
190
|
+
}
|
|
217
191
|
}
|
|
218
192
|
for (const [address, state] of latest) {
|
|
219
193
|
if (state.timestampMs < cutoff) latest.delete(address)
|
|
@@ -223,17 +197,17 @@ function createViewerHistory ({ userDir, nodeId, gatewayId, onError, now = Date.
|
|
|
223
197
|
|
|
224
198
|
async function initialize () {
|
|
225
199
|
if (initialized) return
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
if (/^\d+\.jsonl\.[0-9a-f-]+\.tmp$/.test(name))
|
|
200
|
+
for (const name of storage.listDirectory(directory)) {
|
|
201
|
+
// Left over by the compaction of previous versions.
|
|
202
|
+
if (/^\d+\.jsonl\.[0-9a-f-]+\.tmp$/.test(name)) storage.deleteFile(path.join(directory, name))
|
|
229
203
|
}
|
|
230
|
-
for (const file of await files()) await repairIncompleteTail(file.filename)
|
|
204
|
+
for (const file of await files()) await repairIncompleteTail(storage, file.filename)
|
|
231
205
|
await prune(true)
|
|
232
206
|
latest.clear()
|
|
233
207
|
hours.clear()
|
|
234
208
|
const cutoff = clock() - HISTORY_TTL_MS
|
|
235
209
|
for (const file of await files()) {
|
|
236
|
-
for await (const line of forwardLines(file.filename)) {
|
|
210
|
+
for await (const line of forwardLines(storage, file.filename)) {
|
|
237
211
|
const entry = parseEntry(line)
|
|
238
212
|
if (!entry || entry.timestampMs < cutoff) continue
|
|
239
213
|
sequence = Math.max(sequence, sequenceOf(entry))
|
|
@@ -324,7 +298,7 @@ function createViewerHistory ({ userDir, nodeId, gatewayId, onError, now = Date.
|
|
|
324
298
|
if (Buffer.byteLength(line) > MAX_LINE_BYTES) throw new Error('KNX Viewer history entry exceeds 1 MiB')
|
|
325
299
|
const filename = path.join(directory, `${Math.floor(timestampMs / HOUR_MS)}.jsonl`)
|
|
326
300
|
try {
|
|
327
|
-
|
|
301
|
+
storage.appendFile(filename, line, FILE_OPTIONS)
|
|
328
302
|
} catch (error) {
|
|
329
303
|
// A failed write can leave half a line; repair it before a later retry.
|
|
330
304
|
initialized = false
|
|
@@ -358,10 +332,8 @@ function createViewerHistory ({ userDir, nodeId, gatewayId, onError, now = Date.
|
|
|
358
332
|
const cutoff = timestamp - HISTORY_TTL_MS
|
|
359
333
|
for (const file of (await files()).reverse()) {
|
|
360
334
|
if ((file.hour + 1) * HOUR_MS <= cutoff || (cursor && file.hour * HOUR_MS > cursor.timestampMs)) continue
|
|
361
|
-
const handle = await
|
|
362
|
-
|
|
363
|
-
snapshots.push(snapshot)
|
|
364
|
-
snapshot.size = (await handle.stat()).size
|
|
335
|
+
const handle = await storage.openSnapshot(file.filename)
|
|
336
|
+
snapshots.push({ handle, size: handle.size, ordered: hours.get(file.hour)?.ordered === true })
|
|
365
337
|
}
|
|
366
338
|
return { timestamp, cutoff }
|
|
367
339
|
}, null)
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"engines": {
|
|
4
4
|
"node": ">=20.18.1"
|
|
5
5
|
},
|
|
6
|
-
"version": "8.
|
|
6
|
+
"version": "8.3.0",
|
|
7
7
|
"description": "KNX integration for Node-RED with KNX/IP and KNX Secure, ETS import, routing, diagnostics, automation utilities, MQTT and Modbus adapters. Optional Hue Ultimate and Matter Ultimate packages connect through the KNX gateway.",
|
|
8
8
|
"files": [
|
|
9
9
|
"nodes/",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"scripts/check-legacy-flows.js"
|
|
20
20
|
],
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"knxultimate": "6.
|
|
22
|
+
"knxultimate": "6.3.0",
|
|
23
23
|
"lodash": "4.18.1",
|
|
24
24
|
"mqtt": "^5.15.2",
|
|
25
25
|
"ping": "0.4.4",
|
package/resources/htmlUtils.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/* global window, document, MutationObserver, $, jQuery */
|
|
1
|
+
/* global window, document, MutationObserver, $, jQuery, RED */
|
|
2
2
|
/* eslint-disable camelcase */
|
|
3
3
|
|
|
4
4
|
// Keep informational tips aligned with the full width of our Node-RED editor forms.
|
|
@@ -8,7 +8,9 @@
|
|
|
8
8
|
if (document.getElementById('knx-ultimate-editor-styles')) return
|
|
9
9
|
const style = document.createElement('style')
|
|
10
10
|
style.id = 'knx-ultimate-editor-styles'
|
|
11
|
-
style.textContent = '.form-tips { width: 100% !important; max-width: none !important; box-sizing: border-box !important; background-color: #e8f5e9 !important; }'
|
|
11
|
+
style.textContent = '.form-tips { width: 100% !important; max-width: none !important; box-sizing: border-box !important; background-color: #e8f5e9 !important; }' +
|
|
12
|
+
// File Cache Control: warning tip shown when the gateway does not write files.
|
|
13
|
+
' .form-tips.knx-file-cache-disabled-tip { background-color: #fde4e1 !important; border-color: #f2a39b !important; color: #8a1c12 !important; }'
|
|
12
14
|
document.head.appendChild(style)
|
|
13
15
|
})()
|
|
14
16
|
|
|
@@ -103,6 +105,26 @@ function KNX_cleanAutocompleteTerm (term) {
|
|
|
103
105
|
|
|
104
106
|
window.KNX_cleanAutocompleteTerm = KNX_cleanAutocompleteTerm
|
|
105
107
|
|
|
108
|
+
// File Cache Control: show a tip when the selected gateway does not write files to disk.
|
|
109
|
+
// isRelevant() tells whether the node currently uses files; returns the refresh function.
|
|
110
|
+
function KNX_bindFileCacheDisabledTip ($tip, $server, isRelevant) {
|
|
111
|
+
if (!($tip && $tip.length)) return function () { }
|
|
112
|
+
$tip.addClass('knx-file-cache-disabled-tip').text(RED._('node-red-contrib-knx-ultimate/knxUltimate-config:knxUltimate-config.file_cache.disabled_tip'))
|
|
113
|
+
const refresh = function () {
|
|
114
|
+
let disabled = false
|
|
115
|
+
try {
|
|
116
|
+
const gateway = RED.nodes.node($server.val())
|
|
117
|
+
disabled = Boolean(gateway && gateway.fileCacheMode === 'disabled')
|
|
118
|
+
} catch (e) { }
|
|
119
|
+
$tip.toggle(disabled && (typeof isRelevant !== 'function' || isRelevant() === true))
|
|
120
|
+
}
|
|
121
|
+
$server.on('change.knxFileCacheTip', refresh)
|
|
122
|
+
refresh()
|
|
123
|
+
return refresh
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
window.KNX_bindFileCacheDisabledTip = KNX_bindFileCacheDisabledTip
|
|
127
|
+
|
|
106
128
|
// 2025-09: Make DPT selects searchable via jQuery UI Autocomplete
|
|
107
129
|
function KNX_makeSelectSearchable ($select) {
|
|
108
130
|
try {
|