node-red-contrib-knx-ultimate 4.1.27 → 4.1.29

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 (31) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/examples/KNX DateTime - Set Date and Time to Bus.json +89 -0
  3. package/nodes/knxUltimateDateTime.html +559 -0
  4. package/nodes/knxUltimateDateTime.js +306 -0
  5. package/nodes/knxUltimateLoadControl.html +38 -7
  6. package/nodes/knxUltimateLoadControl.js +38 -2
  7. package/nodes/locales/de/knxUltimateDateTime.html +36 -0
  8. package/nodes/locales/de/knxUltimateDateTime.json +30 -0
  9. package/nodes/locales/de/knxUltimateLoadControl.html +2 -1
  10. package/nodes/locales/de/knxUltimateLoadControl.json +5 -1
  11. package/nodes/locales/en/knxUltimateDateTime.html +45 -0
  12. package/nodes/locales/en/knxUltimateDateTime.json +30 -0
  13. package/nodes/locales/en/knxUltimateLoadControl.html +2 -1
  14. package/nodes/locales/en/knxUltimateLoadControl.json +5 -1
  15. package/nodes/locales/es/knxUltimateDateTime.html +36 -0
  16. package/nodes/locales/es/knxUltimateDateTime.json +30 -0
  17. package/nodes/locales/es/knxUltimateLoadControl.html +2 -1
  18. package/nodes/locales/es/knxUltimateLoadControl.json +5 -1
  19. package/nodes/locales/fr/knxUltimateDateTime.html +36 -0
  20. package/nodes/locales/fr/knxUltimateDateTime.json +30 -0
  21. package/nodes/locales/fr/knxUltimateLoadControl.html +2 -1
  22. package/nodes/locales/fr/knxUltimateLoadControl.json +5 -1
  23. package/nodes/locales/it/knxUltimateDateTime.html +45 -0
  24. package/nodes/locales/it/knxUltimateDateTime.json +30 -0
  25. package/nodes/locales/it/knxUltimateLoadControl.html +2 -1
  26. package/nodes/locales/it/knxUltimateLoadControl.json +5 -1
  27. package/nodes/locales/zh-CN/knxUltimateDateTime.html +36 -0
  28. package/nodes/locales/zh-CN/knxUltimateDateTime.json +30 -0
  29. package/nodes/locales/zh-CN/knxUltimateLoadControl.html +2 -1
  30. package/nodes/locales/zh-CN/knxUltimateLoadControl.json +5 -1
  31. package/package.json +3 -2
package/CHANGELOG.md CHANGED
@@ -6,6 +6,19 @@
6
6
 
7
7
  # CHANGELOG
8
8
 
9
+ **Version 4.1.29** - March 2026<br/>
10
+
11
+ - NEW: **KNX DateTime** node: set date/time on the KNX bus via **DPT 19.001** (DateTime) and optionally **DPT 11.001** (Date) / **DPT 10.001** (Time).<br/>
12
+ - NEW: **KNX DateTime**: send on startup (with delay), periodic send, editor send-now button, and input-triggered send.<br/>
13
+ - UI: **KNX DateTime**: when adding a new node, it can auto-select the first KNX Gateway with an ETS import and pre-fill coherent group addresses.<br/>
14
+ - UI: **KNX DateTime**: auto-fill is also triggered when changing the selected KNX Gateway (it won’t override manual values).<br/>
15
+ - i18n/help/wiki: added **KNX DateTime** help and docs pages in all supported languages, and added the node to the docs navigation.<br/>
16
+
17
+ **Version 4.1.28** - February 2026<br/>
18
+
19
+ - NEW: **KNX Load Control**: added **Mode** selector to disable the internal logic and use only `msg.shedding` commands (`shed`/`unshed`).<br/>
20
+ - UI/Docs/help/wiki: updated **KNX Load Control** editor, help and docs pages in all supported languages.<br/>
21
+
9
22
  **Version 4.1.27** - February 2026<br/>
10
23
 
11
24
  - Bumped KNX Engine to 5.2.8<br/>
@@ -0,0 +1,89 @@
1
+ [{
2
+ "id": "dt_cfg_1",
3
+ "type": "knxUltimate-config",
4
+ "z": "",
5
+ "host": "224.0.23.12",
6
+ "port": "3671",
7
+ "physAddr": "15.15.202",
8
+ "suppressACKRequest": false,
9
+ "csv": "",
10
+ "KNXEthInterface": "Auto",
11
+ "KNXEthInterfaceManuallyInput": "",
12
+ "autoReconnect": "yes"
13
+ },
14
+ {
15
+ "id": "dt_inj_now",
16
+ "type": "inject",
17
+ "z": "flow_dt",
18
+ "name": "Send NOW",
19
+ "props": [
20
+ { "p": "payload" }
21
+ ],
22
+ "repeat": "",
23
+ "crontab": "",
24
+ "once": false,
25
+ "onceDelay": 0.1,
26
+ "topic": "",
27
+ "payloadType": "date",
28
+ "x": 150,
29
+ "y": 160,
30
+ "wires": [["dt_node"]]
31
+ },
32
+ {
33
+ "id": "dt_inj_custom",
34
+ "type": "inject",
35
+ "z": "flow_dt",
36
+ "name": "Send fixed ISO date",
37
+ "props": [
38
+ { "p": "payload", "v": "2026-03-02T10:00:00+01:00", "vt": "str" }
39
+ ],
40
+ "repeat": "",
41
+ "crontab": "",
42
+ "once": false,
43
+ "onceDelay": 0.1,
44
+ "topic": "",
45
+ "x": 170,
46
+ "y": 220,
47
+ "wires": [["dt_node"]]
48
+ },
49
+ {
50
+ "id": "dt_node",
51
+ "type": "knxUltimateDateTime",
52
+ "z": "flow_dt",
53
+ "server": "dt_cfg_1",
54
+ "name": "KNX Clock",
55
+ "outputtopic": "events/knx/datetime",
56
+ "gaDateTime": "1/7/1",
57
+ "nameDateTime": "Bus DateTime",
58
+ "dptDateTime": "19.001",
59
+ "gaDate": "",
60
+ "nameDate": "",
61
+ "dptDate": "11.001",
62
+ "gaTime": "",
63
+ "nameTime": "",
64
+ "dptTime": "10.001",
65
+ "sendOnDeploy": true,
66
+ "sendOnDeployDelay": 2,
67
+ "periodicSend": false,
68
+ "periodicSendInterval": 60,
69
+ "periodicSendUnit": "m",
70
+ "x": 420,
71
+ "y": 190,
72
+ "wires": [["dt_dbg"]]
73
+ },
74
+ {
75
+ "id": "dt_dbg",
76
+ "type": "debug",
77
+ "z": "flow_dt",
78
+ "name": "DateTime out",
79
+ "active": true,
80
+ "tosidebar": true,
81
+ "console": false,
82
+ "tostatus": true,
83
+ "complete": "true",
84
+ "targetType": "full",
85
+ "x": 640,
86
+ "y": 190,
87
+ "wires": []
88
+ }]
89
+
@@ -0,0 +1,559 @@
1
+ <script type="text/javascript" src="resources/node-red-contrib-knx-ultimate/htmlUtils.js"></script>
2
+
3
+ <script type="text/javascript">
4
+ const KNX_ULTIMATE_DATETIME_KEYWORDS = {
5
+ datetime: [/date\s*\/\s*time/i, /date\s*time/i, /datetime/i, /data\s*\/\s*ora/i, /data\s*ora/i],
6
+ date: [/\bdate\b/i, /\bdata\b/i, /\bdatum\b/i, /\bfecha\b/i],
7
+ time: [/\btime\b/i, /\bora\b/i, /\bheure\b/i, /\bzeit\b/i, /\bhora\b/i, /\borologio\b/i, /\bclock\b/i]
8
+ }
9
+
10
+ const KNX_ULTIMATE_DATETIME_STOPWORDS = new Set([
11
+ 'date', 'datetime', 'time', 'data', 'ora', 'orologio', 'clock', 'bus', 'knx', 'set', 'sync', 'sincro', 'synchronization'
12
+ ])
13
+
14
+ const knxDateTimeNormalizeTokens = (value) => {
15
+ const str = (value || '').toString().toLowerCase()
16
+ const cleaned = str
17
+ .replace(/dpt\s*\d+(\.\d+)?/g, ' ')
18
+ .replace(/[^a-z0-9]+/g, ' ')
19
+ .trim()
20
+ if (!cleaned) return []
21
+ return cleaned
22
+ .split(/\s+/)
23
+ .map((t) => t.trim())
24
+ .filter((t) => t.length > 1 && !KNX_ULTIMATE_DATETIME_STOPWORDS.has(t))
25
+ }
26
+
27
+ const knxDateTimeTokenSimilarity = (aTokens, bTokens) => {
28
+ if (!Array.isArray(aTokens) || !Array.isArray(bTokens) || aTokens.length === 0 || bTokens.length === 0) return 0
29
+ const a = new Set(aTokens)
30
+ const b = new Set(bTokens)
31
+ let inter = 0
32
+ a.forEach((t) => { if (b.has(t)) inter += 1 })
33
+ const union = a.size + b.size - inter
34
+ return union > 0 ? inter / union : 0
35
+ }
36
+
37
+ const knxDateTimeParseGA = (ga) => {
38
+ const parts = (ga || '').toString().trim().split('/')
39
+ if (parts.length !== 3) return null
40
+ const nums = parts.map((p) => Number(p))
41
+ if (nums.some((n) => !Number.isInteger(n) || n < 0)) return null
42
+ return nums
43
+ }
44
+
45
+ const knxDateTimeCompareGA = (a, b) => {
46
+ const aa = knxDateTimeParseGA(a)
47
+ const bb = knxDateTimeParseGA(b)
48
+ if (!aa && !bb) return 0
49
+ if (!aa) return 1
50
+ if (!bb) return -1
51
+ for (let i = 0; i < 3; i++) {
52
+ if (aa[i] !== bb[i]) return aa[i] - bb[i]
53
+ }
54
+ return 0
55
+ }
56
+
57
+ const knxGetKnxUltimateConfigs = () => {
58
+ const configs = []
59
+ try {
60
+ if (RED && RED.nodes) {
61
+ if (typeof RED.nodes.eachConfig === 'function') {
62
+ RED.nodes.eachConfig((cfg) => {
63
+ if (cfg && cfg.type === 'knxUltimate-config') configs.push(cfg)
64
+ })
65
+ } else if (typeof RED.nodes.eachNode === 'function') {
66
+ RED.nodes.eachNode((n) => {
67
+ if (n && n.type === 'knxUltimate-config') configs.push(n)
68
+ })
69
+ }
70
+ if (configs.length === 0 && typeof RED.nodes.filterNodes === 'function') {
71
+ try {
72
+ const filtered = RED.nodes.filterNodes({ type: 'knxUltimate-config' })
73
+ if (Array.isArray(filtered)) filtered.forEach((n) => configs.push(n))
74
+ } catch (error) { /* ignore */ }
75
+ }
76
+ }
77
+ } catch (error) { /* ignore */ }
78
+ return configs
79
+ }
80
+
81
+ const knxFetchGroupAddresses = (serverId) => {
82
+ return new Promise((resolve) => {
83
+ if (!serverId) return resolve([])
84
+ $.getJSON(`knxUltimatecsv?nodeID=${serverId}&_=${Date.now()}`, (data) => {
85
+ resolve(Array.isArray(data) ? data : [])
86
+ }).fail(() => resolve([]))
87
+ })
88
+ }
89
+
90
+ const knxScoreEntry = (entry, kind, baseTokens) => {
91
+ if (!entry) return -1
92
+ const dpt = (entry.dpt || '').toString()
93
+ const name = (entry.devicename || '').toString()
94
+ const tokens = knxDateTimeNormalizeTokens(name)
95
+
96
+ let score = 0
97
+
98
+ if (kind === 'datetime') {
99
+ if (dpt === '19.001') score += 60
100
+ else if (dpt.startsWith('19.')) score += 45
101
+ KNX_ULTIMATE_DATETIME_KEYWORDS.datetime.forEach((re) => { if (re.test(name)) score += 20 })
102
+ KNX_ULTIMATE_DATETIME_KEYWORDS.time.forEach((re) => { if (re.test(name)) score += 6 })
103
+ KNX_ULTIMATE_DATETIME_KEYWORDS.date.forEach((re) => { if (re.test(name)) score += 6 })
104
+ } else if (kind === 'date') {
105
+ if (dpt === '11.001') score += 60
106
+ else if (dpt.startsWith('11.')) score += 45
107
+ KNX_ULTIMATE_DATETIME_KEYWORDS.date.forEach((re) => { if (re.test(name)) score += 18 })
108
+ } else if (kind === 'time') {
109
+ if (dpt === '10.001') score += 60
110
+ else if (dpt.startsWith('10.')) score += 45
111
+ KNX_ULTIMATE_DATETIME_KEYWORDS.time.forEach((re) => { if (re.test(name)) score += 18 })
112
+ }
113
+
114
+ if (Array.isArray(baseTokens) && baseTokens.length > 0) {
115
+ score += Math.round(knxDateTimeTokenSimilarity(tokens, baseTokens) * 30)
116
+ }
117
+
118
+ return score
119
+ }
120
+
121
+ const knxPickBest = (entries, kind, baseTokens) => {
122
+ if (!Array.isArray(entries) || entries.length === 0) return null
123
+ let best = null
124
+ let bestScore = -1
125
+ entries.forEach((e) => {
126
+ const s = knxScoreEntry(e, kind, baseTokens)
127
+ if (s > bestScore) {
128
+ bestScore = s
129
+ best = e
130
+ } else if (s === bestScore && best) {
131
+ const cmp = knxDateTimeCompareGA(e.ga, best.ga)
132
+ if (cmp < 0) best = e
133
+ }
134
+ })
135
+ return best
136
+ }
137
+
138
+ const knxSuggestFromCsv = (csvRows) => {
139
+ const rows = Array.isArray(csvRows) ? csvRows : []
140
+ const datetimeRows = rows.filter((r) => (r && typeof r.dpt === 'string' && r.dpt.startsWith('19.')))
141
+ const dateRows = rows.filter((r) => (r && typeof r.dpt === 'string' && r.dpt.startsWith('11.')))
142
+ const timeRows = rows.filter((r) => (r && typeof r.dpt === 'string' && r.dpt.startsWith('10.')))
143
+
144
+ const bestDateTime = knxPickBest(datetimeRows, 'datetime', [])
145
+ const baseTokens = bestDateTime ? knxDateTimeNormalizeTokens(bestDateTime.devicename || '') : []
146
+
147
+ const bestDate = knxPickBest(dateRows, 'date', baseTokens)
148
+ const bestTime = knxPickBest(timeRows, 'time', baseTokens)
149
+
150
+ return {
151
+ dateTime: bestDateTime,
152
+ date: bestDate,
153
+ time: bestTime
154
+ }
155
+ }
156
+
157
+ const knxAutoConfigureDateTimeNode = async (node, { updateDom = false, preferExistingServer = true } = {}) => {
158
+ try {
159
+ if (!node) return
160
+
161
+ const hasAnyGA = !!((node.gaDateTime || '').trim() || (node.gaDate || '').trim() || (node.gaTime || '').trim())
162
+ if (hasAnyGA) return
163
+
164
+ // If server already selected and it has ETS rows, reuse it.
165
+ const currentServerId = preferExistingServer ? (node.server || '') : ''
166
+ if (currentServerId && currentServerId !== '_ADD_') {
167
+ const rows = await knxFetchGroupAddresses(currentServerId)
168
+ if (rows.length > 0) {
169
+ const suggestions = knxSuggestFromCsv(rows)
170
+ return knxApplySuggestions(node, currentServerId, suggestions, { updateDom })
171
+ }
172
+ }
173
+
174
+ // Otherwise, select the first knxUltimate-config that has an ETS CSV imported (non-empty parsed GA list).
175
+ const configs = knxGetKnxUltimateConfigs()
176
+ if (configs.length === 0) return
177
+
178
+ // Fast path: config node already carries an ETS file/path in its `csv` property.
179
+ for (let i = 0; i < configs.length; i++) {
180
+ const cfg = configs[i]
181
+ const id = cfg && cfg.id ? cfg.id : null
182
+ const csvHint = cfg && typeof cfg.csv === 'string' ? cfg.csv.trim() : ''
183
+ if (!id || !csvHint) continue
184
+ const rows = await knxFetchGroupAddresses(id)
185
+ if (rows.length === 0) continue
186
+ const suggestions = knxSuggestFromCsv(rows)
187
+ return knxApplySuggestions(node, id, suggestions, { updateDom })
188
+ }
189
+
190
+ const maxCandidates = Math.min(10, configs.length)
191
+ const checks = configs.slice(0, maxCandidates).map((cfg) => {
192
+ const id = cfg && cfg.id ? cfg.id : null
193
+ return knxFetchGroupAddresses(id).then((rows) => ({ id, rows }))
194
+ })
195
+ const results = await Promise.all(checks)
196
+
197
+ let selected = null
198
+ for (let i = 0; i < results.length; i++) {
199
+ if (results[i] && results[i].id && Array.isArray(results[i].rows) && results[i].rows.length > 0) {
200
+ selected = results[i]
201
+ break
202
+ }
203
+ }
204
+ if (!selected) return
205
+
206
+ const suggestions = knxSuggestFromCsv(selected.rows)
207
+ return knxApplySuggestions(node, selected.id, suggestions, { updateDom })
208
+ } catch (error) {
209
+ try { console.warn('knxUltimateDateTime auto-config failed', error) } catch (e) { /* ignore */ }
210
+ }
211
+ }
212
+
213
+ const knxAutoConfigureDateTimeNodeForServer = async (node, serverId, { updateDom = false, overwrite = false } = {}) => {
214
+ try {
215
+ if (!node || !serverId) return
216
+ const rows = await knxFetchGroupAddresses(serverId)
217
+ if (!Array.isArray(rows) || rows.length === 0) return
218
+ const suggestions = knxSuggestFromCsv(rows)
219
+ return knxApplySuggestions(node, serverId, suggestions, { updateDom, overwrite })
220
+ } catch (error) {
221
+ try { console.warn('knxUltimateDateTime auto-config for server failed', error) } catch (e) { /* ignore */ }
222
+ }
223
+ }
224
+
225
+ const knxApplySuggestions = (node, serverId, suggestions, { updateDom = false, overwrite = false } = {}) => {
226
+ if (!node || !serverId) return
227
+ if (!suggestions) return
228
+
229
+ // Avoid repeating the same automation multiple times.
230
+ if (node._knxDateTimeAutoConfigured === true && overwrite !== true) return
231
+
232
+ node.server = serverId
233
+
234
+ if (suggestions.dateTime && suggestions.dateTime.ga && (overwrite || !(node.gaDateTime || '').trim())) {
235
+ node.gaDateTime = suggestions.dateTime.ga
236
+ node.nameDateTime = suggestions.dateTime.devicename || ''
237
+ }
238
+ if (suggestions.date && suggestions.date.ga && (overwrite || !(node.gaDate || '').trim())) {
239
+ node.gaDate = suggestions.date.ga
240
+ node.nameDate = suggestions.date.devicename || ''
241
+ }
242
+ if (suggestions.time && suggestions.time.ga && (overwrite || !(node.gaTime || '').trim())) {
243
+ node.gaTime = suggestions.time.ga
244
+ node.nameTime = suggestions.time.devicename || ''
245
+ }
246
+
247
+ node._knxDateTimeAutoConfigured = true
248
+ node._knxDateTimeAutoConfiguredServer = serverId
249
+
250
+ if (updateDom) {
251
+ try {
252
+ $('#node-input-server').val(serverId).trigger('change')
253
+ if (node.gaDateTime) $('#node-input-gaDateTime').val(node.gaDateTime)
254
+ if (node.nameDateTime) $('#node-input-nameDateTime').val(node.nameDateTime)
255
+ if (node.gaDate) $('#node-input-gaDate').val(node.gaDate)
256
+ if (node.nameDate) $('#node-input-nameDate').val(node.nameDate)
257
+ if (node.gaTime) $('#node-input-gaTime').val(node.gaTime)
258
+ if (node.nameTime) $('#node-input-nameTime').val(node.nameTime)
259
+ } catch (error) { /* ignore */ }
260
+ } else {
261
+ try {
262
+ if (RED && RED.nodes && typeof RED.nodes.dirty === 'function') RED.nodes.dirty(true)
263
+ } catch (error) { /* ignore */ }
264
+ try { if (RED && RED.view && typeof RED.view.redraw === 'function') RED.view.redraw() } catch (error) { /* ignore */ }
265
+ }
266
+ }
267
+
268
+ RED.nodes.registerType('knxUltimateDateTime', {
269
+ category: 'KNX Ultimate',
270
+ color: '#C7E9C0',
271
+ defaults: {
272
+ server: { type: 'knxUltimate-config', required: true },
273
+ name: { value: '' },
274
+ outputtopic: { value: '' },
275
+ gaDateTime: { value: '' },
276
+ nameDateTime: { value: '' },
277
+ dptDateTime: { value: '19.001' },
278
+ gaDate: { value: '' },
279
+ nameDate: { value: '' },
280
+ dptDate: { value: '11.001' },
281
+ gaTime: { value: '' },
282
+ nameTime: { value: '' },
283
+ dptTime: { value: '10.001' },
284
+ sendOnDeploy: { value: true },
285
+ sendOnDeployDelay: { value: 1, validate: RED.validators.number() },
286
+ periodicSend: { value: true },
287
+ periodicSendInterval: { value: 60, validate: RED.validators.number() },
288
+ periodicSendUnit: { value: 'm' }
289
+ },
290
+ inputs: 0,
291
+ outputs: 0,
292
+ icon: 'node-knx-icon.svg',
293
+ label: function () {
294
+ return this.name || 'KNX DateTime'
295
+ },
296
+ paletteLabel: function () {
297
+ try {
298
+ return RED._('node-red-contrib-knx-ultimate/knxUltimateDateTime:knxUltimateDateTime.paletteLabel') || 'DateTime'
299
+ } catch (error) {
300
+ return 'DateTime'
301
+ }
302
+ },
303
+ onadd: function () {
304
+ // Auto-select a KNX gateway (first one with ETS CSV imported) and prefill coherent group addresses.
305
+ // This runs when the node is dragged from the palette. Best-effort, non-blocking.
306
+ const node = this
307
+ setTimeout(() => { knxAutoConfigureDateTimeNode(node, { updateDom: false, preferExistingServer: true }) }, 50)
308
+ },
309
+ button: {
310
+ enabled: function () {
311
+ return !this.changed
312
+ },
313
+ visible: function () {
314
+ return true
315
+ },
316
+ onclick: function () {
317
+ const node = this
318
+ $.ajax({
319
+ type: 'POST',
320
+ url: 'knxUltimateDateTime/sendNow',
321
+ data: { id: node.id },
322
+ success: function (response) {
323
+ const queued = response && response.queued === true
324
+ const message = queued
325
+ ? (RED._('node-red-contrib-knx-ultimate/knxUltimateDateTime:knxUltimateDateTime.notifyQueued') || 'Queued (gateway not connected yet)')
326
+ : (RED._('node-red-contrib-knx-ultimate/knxUltimateDateTime:knxUltimateDateTime.notifySent') || 'Sent to KNX')
327
+ RED.notify(message, 'success')
328
+ },
329
+ error: function (xhr) {
330
+ let message = 'Error'
331
+ try {
332
+ if (xhr && xhr.responseJSON && xhr.responseJSON.error) message = xhr.responseJSON.error
333
+ } catch (error) { /* ignore */ }
334
+ RED.notify(message, 'error')
335
+ }
336
+ })
337
+ }
338
+ },
339
+ oneditprepare: function () {
340
+ const node = this
341
+ const $knxServerInput = $('#node-input-server')
342
+ const KNX_EMPTY_VALUES = new Set(['', '_ADD_', '__NONE__', 'none'])
343
+ const KNX_GA_CACHE = node._knxGaCache || (node._knxGaCache = new Map())
344
+
345
+ try { RED.sidebar.show('help') } catch (error) { /* ignore */ }
346
+
347
+ const resolveKnxServerValue = () => {
348
+ const domValue = $knxServerInput.val()
349
+ if (domValue !== undefined && domValue !== null && domValue !== '') return domValue
350
+ if (node.server !== undefined && node.server !== null && node.server !== '') return node.server
351
+ return ''
352
+ }
353
+
354
+ const hasKnxServerSelected = () => {
355
+ const val = resolveKnxServerValue()
356
+ return !(val === undefined || val === null || KNX_EMPTY_VALUES.has(String(val)))
357
+ }
358
+
359
+ const fetchGroupAddresses = (serverId) => {
360
+ if (!serverId) return Promise.resolve([])
361
+ if (KNX_GA_CACHE.has(serverId)) return Promise.resolve(KNX_GA_CACHE.get(serverId))
362
+ return new Promise((resolve) => {
363
+ $.getJSON(`knxUltimatecsv?nodeID=${serverId}&_=${Date.now()}`, (data) => {
364
+ const list = Array.isArray(data) ? data : []
365
+ KNX_GA_CACHE.set(serverId, list)
366
+ resolve(list)
367
+ }).fail(() => resolve([]))
368
+ })
369
+ }
370
+
371
+ const setupGA = (gaSelector, nameSelector, dptSelector, allowedPrefixes, defaultDpt) => {
372
+ const $gaInput = $(gaSelector)
373
+ const $nameInput = $(nameSelector)
374
+ const $dptInput = $(dptSelector)
375
+ if (!$gaInput.length) return
376
+
377
+ if ($dptInput.length && (!$dptInput.val() || $dptInput.val() === '')) $dptInput.val(defaultDpt)
378
+
379
+ const sourceFn = (request, response) => {
380
+ if (!hasKnxServerSelected()) {
381
+ response([])
382
+ return
383
+ }
384
+ const serverId = resolveKnxServerValue()
385
+ fetchGroupAddresses(serverId).then((data) => {
386
+ const items = []
387
+ data.forEach((entry) => {
388
+ const dpt = entry.dpt || ''
389
+ const allowed = allowedPrefixes.some((prefix) => prefix === '' || dpt.startsWith(prefix))
390
+ if (!allowed) return
391
+ const devName = entry.devicename || ''
392
+ const searchStr = `${entry.ga} (${devName}) DPT${dpt}`
393
+ if (!htmlUtilsfullCSVSearch(searchStr, request.term || '')) return
394
+ items.push({
395
+ label: `${entry.ga} # ${devName} # ${dpt}`,
396
+ value: entry.ga,
397
+ ga: entry.ga
398
+ })
399
+ })
400
+ response(items)
401
+ })
402
+ }
403
+
404
+ if ($gaInput.data('knx-ga-initialised')) {
405
+ $gaInput.autocomplete('option', 'source', sourceFn)
406
+ } else {
407
+ $gaInput
408
+ .autocomplete({
409
+ minLength: 0,
410
+ source: sourceFn,
411
+ select: (event, ui) => {
412
+ let deviceName = ''
413
+ try {
414
+ deviceName = ui.item.label.split('#')[1].trim()
415
+ } catch (error) { deviceName = '' }
416
+ if ($nameInput.length) {
417
+ $nameInput.val(deviceName || '')
418
+ }
419
+ try {
420
+ const parts = ui.item.label.split('#')
421
+ const dptFromLabel = parts.length >= 3 ? parts[2].trim() : ''
422
+ $dptInput.val(dptFromLabel || defaultDpt)
423
+ } catch (error) {
424
+ $dptInput.val(defaultDpt)
425
+ }
426
+ }
427
+ })
428
+ .on('focus.knxUltimateDateTime click.knxUltimateDateTime', function () {
429
+ const currentValue = $(this).val() || ''
430
+ try { $(this).autocomplete('search', `${currentValue} exactmatch`) } catch (error) { /* ignore */ }
431
+ })
432
+ $gaInput.data('knx-ga-initialised', true)
433
+ }
434
+
435
+ try {
436
+ if (hasKnxServerSelected()) {
437
+ const srv = RED.nodes.node(resolveKnxServerValue())
438
+ if (srv && srv.id) KNX_enableSecureFormatting($gaInput, srv.id)
439
+ }
440
+ } catch (error) { /* ignore */ }
441
+ }
442
+
443
+ const refresh = () => {
444
+ setupGA('#node-input-gaDateTime', '#node-input-nameDateTime', '#node-input-dptDateTime', ['19.'], '19.001')
445
+ setupGA('#node-input-gaDate', '#node-input-nameDate', '#node-input-dptDate', ['11.'], '11.001')
446
+ setupGA('#node-input-gaTime', '#node-input-nameTime', '#node-input-dptTime', ['10.'], '10.001')
447
+ }
448
+
449
+ $knxServerInput.on('change', () => {
450
+ KNX_GA_CACHE.clear()
451
+ refresh()
452
+ try {
453
+ const sid = resolveKnxServerValue()
454
+ if (!sid || sid === '_ADD_') return
455
+ const hasAnyGAInUi = !!(
456
+ ($('#node-input-gaDateTime').val() || '').toString().trim() ||
457
+ ($('#node-input-gaDate').val() || '').toString().trim() ||
458
+ ($('#node-input-gaTime').val() || '').toString().trim()
459
+ )
460
+ // If the current values were auto-filled, allow overwrite when server changes.
461
+ const shouldOverwrite = node._knxDateTimeAutoConfigured === true && node._knxDateTimeAutoConfiguredServer && node._knxDateTimeAutoConfiguredServer !== sid
462
+ // Otherwise fill only empty fields (do not override manual config).
463
+ knxAutoConfigureDateTimeNodeForServer(node, sid, { updateDom: true, overwrite: shouldOverwrite || !hasAnyGAInUi })
464
+ } catch (error) { /* ignore */ }
465
+ })
466
+
467
+ refresh()
468
+
469
+ // Auto-select server + fill GAs only for a brand new node (all GAs empty).
470
+ setTimeout(() => { knxAutoConfigureDateTimeNode(node, { updateDom: true, preferExistingServer: true }) }, 50)
471
+
472
+ const syncUi = () => {
473
+ const sendOnDeploy = $('#node-input-sendOnDeploy').is(':checked')
474
+ $('.knx-datetime-deploy-options').toggle(sendOnDeploy)
475
+ const periodicSend = $('#node-input-periodicSend').is(':checked')
476
+ $('.knx-datetime-periodic-options').toggle(periodicSend)
477
+ }
478
+
479
+ $('#node-input-sendOnDeploy').on('change', syncUi)
480
+ $('#node-input-periodicSend').on('change', syncUi)
481
+ syncUi()
482
+ }
483
+ })
484
+ </script>
485
+
486
+ <script type="text/html" data-template-name="knxUltimateDateTime">
487
+ <div class="form-row" style="display:flex; align-items:center;">
488
+ <label for="node-input-server" style="width:180px"><i class="fa fa-circle-o"></i> <span data-i18n="knxUltimateDateTime.node-input-server"></span></label>
489
+ <input type="text" id="node-input-server" style="flex:1">
490
+ </div>
491
+
492
+ <div class="form-row" style="display:flex; align-items:center;">
493
+ <label for="node-input-name" style="width:180px"><i class="fa fa-tag"></i> <span data-i18n="knxUltimateDateTime.node-input-name"></span></label>
494
+ <input type="text" id="node-input-name" style="flex:1" placeholder="KNX DateTime">
495
+ </div>
496
+
497
+ <div class="form-row" style="display:flex; align-items:center;">
498
+ <label for="node-input-outputtopic" style="width:180px"><i class="fa fa-comment"></i> <span data-i18n="knxUltimateDateTime.node-input-outputtopic"></span></label>
499
+ <input type="text" id="node-input-outputtopic" style="flex:1" placeholder="events/knx/datetime" data-i18n="[placeholder]knxUltimateDateTime.placeholders.outputtopic">
500
+ </div>
501
+
502
+ <hr>
503
+ <div class="form-row" style="margin:4px 0 2px;">
504
+ <span style="font-weight:bold;" data-i18n="knxUltimateDateTime.section_addresses"></span>
505
+ </div>
506
+
507
+ <div class="form-row" style="display:flex; align-items:center; gap:8px;">
508
+ <label for="node-input-gaDateTime" style="width:180px"><i class="fa fa-calendar"></i> <span data-i18n="knxUltimateDateTime.gaDateTime"></span></label>
509
+ <input type="text" id="node-input-gaDateTime" style="width:160px" placeholder="1/7/1" data-i18n="[placeholder]knxUltimateDateTime.placeholders.ga">
510
+ <input type="text" id="node-input-nameDateTime" style="flex:1" placeholder="DateTime object" data-i18n="[placeholder]knxUltimateDateTime.placeholders.nameDateTime">
511
+ <label for="node-input-dptDateTime" style="width:60px; text-align:right">DPT</label>
512
+ <input type="text" id="node-input-dptDateTime" style="width:160px" readonly>
513
+ </div>
514
+
515
+ <div class="form-row" style="display:flex; align-items:center; gap:8px;">
516
+ <label for="node-input-gaDate" style="width:180px"><i class="fa fa-calendar-o"></i> <span data-i18n="knxUltimateDateTime.gaDate"></span></label>
517
+ <input type="text" id="node-input-gaDate" style="width:160px" placeholder="1/7/2" data-i18n="[placeholder]knxUltimateDateTime.placeholders.ga">
518
+ <input type="text" id="node-input-nameDate" style="flex:1" placeholder="Date object" data-i18n="[placeholder]knxUltimateDateTime.placeholders.nameDate">
519
+ <label for="node-input-dptDate" style="width:60px; text-align:right">DPT</label>
520
+ <input type="text" id="node-input-dptDate" style="width:160px" readonly>
521
+ </div>
522
+
523
+ <div class="form-row" style="display:flex; align-items:center; gap:8px;">
524
+ <label for="node-input-gaTime" style="width:180px"><i class="fa fa-clock-o"></i> <span data-i18n="knxUltimateDateTime.gaTime"></span></label>
525
+ <input type="text" id="node-input-gaTime" style="width:160px" placeholder="1/7/3" data-i18n="[placeholder]knxUltimateDateTime.placeholders.ga">
526
+ <input type="text" id="node-input-nameTime" style="flex:1" placeholder="Time object" data-i18n="[placeholder]knxUltimateDateTime.placeholders.nameTime">
527
+ <label for="node-input-dptTime" style="width:60px; text-align:right">DPT</label>
528
+ <input type="text" id="node-input-dptTime" style="width:160px" readonly>
529
+ </div>
530
+
531
+ <hr>
532
+ <div class="form-row" style="margin:4px 0 2px;">
533
+ <span style="font-weight:bold;" data-i18n="knxUltimateDateTime.section_send"></span>
534
+ </div>
535
+
536
+ <div class="form-row" style="display:flex; align-items:center;">
537
+ <label for="node-input-sendOnDeploy" style="width:180px"><i class="fa fa-play"></i> <span data-i18n="knxUltimateDateTime.node-input-sendOnDeploy"></span></label>
538
+ <input type="checkbox" id="node-input-sendOnDeploy" style="width:auto">
539
+ </div>
540
+
541
+ <div class="form-row knx-datetime-deploy-options" style="display:flex; align-items:center;">
542
+ <label for="node-input-sendOnDeployDelay" style="width:180px"><i class="fa fa-clock-o"></i> <span data-i18n="knxUltimateDateTime.node-input-sendOnDeployDelay"></span></label>
543
+ <input type="number" id="node-input-sendOnDeployDelay" style="width:120px">
544
+ </div>
545
+
546
+ <div class="form-row" style="display:flex; align-items:center;">
547
+ <label for="node-input-periodicSend" style="width:180px"><i class="fa fa-repeat"></i> <span data-i18n="knxUltimateDateTime.node-input-periodicSend"></span></label>
548
+ <input type="checkbox" id="node-input-periodicSend" style="width:auto">
549
+ </div>
550
+
551
+ <div class="form-row knx-datetime-periodic-options" style="display:flex; align-items:center; gap:8px;">
552
+ <label for="node-input-periodicSendInterval" style="width:180px"><i class="fa fa-hourglass"></i> <span data-i18n="knxUltimateDateTime.node-input-periodicSendInterval"></span></label>
553
+ <input type="number" id="node-input-periodicSendInterval" style="width:120px">
554
+ <select id="node-input-periodicSendUnit" style="width:160px">
555
+ <option value="s" data-i18n="knxUltimateDateTime.unit_seconds"></option>
556
+ <option value="m" data-i18n="knxUltimateDateTime.unit_minutes"></option>
557
+ </select>
558
+ </div>
559
+ </script>