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.
- package/CHANGELOG.md +13 -0
- package/examples/KNX DateTime - Set Date and Time to Bus.json +89 -0
- package/nodes/knxUltimateDateTime.html +559 -0
- package/nodes/knxUltimateDateTime.js +306 -0
- package/nodes/knxUltimateLoadControl.html +38 -7
- package/nodes/knxUltimateLoadControl.js +38 -2
- package/nodes/locales/de/knxUltimateDateTime.html +36 -0
- package/nodes/locales/de/knxUltimateDateTime.json +30 -0
- package/nodes/locales/de/knxUltimateLoadControl.html +2 -1
- package/nodes/locales/de/knxUltimateLoadControl.json +5 -1
- package/nodes/locales/en/knxUltimateDateTime.html +45 -0
- package/nodes/locales/en/knxUltimateDateTime.json +30 -0
- package/nodes/locales/en/knxUltimateLoadControl.html +2 -1
- package/nodes/locales/en/knxUltimateLoadControl.json +5 -1
- package/nodes/locales/es/knxUltimateDateTime.html +36 -0
- package/nodes/locales/es/knxUltimateDateTime.json +30 -0
- package/nodes/locales/es/knxUltimateLoadControl.html +2 -1
- package/nodes/locales/es/knxUltimateLoadControl.json +5 -1
- package/nodes/locales/fr/knxUltimateDateTime.html +36 -0
- package/nodes/locales/fr/knxUltimateDateTime.json +30 -0
- package/nodes/locales/fr/knxUltimateLoadControl.html +2 -1
- package/nodes/locales/fr/knxUltimateLoadControl.json +5 -1
- package/nodes/locales/it/knxUltimateDateTime.html +45 -0
- package/nodes/locales/it/knxUltimateDateTime.json +30 -0
- package/nodes/locales/it/knxUltimateLoadControl.html +2 -1
- package/nodes/locales/it/knxUltimateLoadControl.json +5 -1
- package/nodes/locales/zh-CN/knxUltimateDateTime.html +36 -0
- package/nodes/locales/zh-CN/knxUltimateDateTime.json +30 -0
- package/nodes/locales/zh-CN/knxUltimateLoadControl.html +2 -1
- package/nodes/locales/zh-CN/knxUltimateLoadControl.json +5 -1
- 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>
|